Skip to content

Add form switcher #1160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 64 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
824ba51
Add form switcher
kalilsn Apr 15, 2025
00807da
Update capabilities
kalilsn Apr 16, 2025
7160549
Use membership formIds to calculate permissions and list of forms in …
kalilsn Apr 21, 2025
bc44327
Restrict related pubs table by selected form
kalilsn Apr 21, 2025
4995854
Fix bugs and add form switcher to create page
kalilsn Apr 21, 2025
3e09375
Remove unused component
kalilsn Apr 21, 2025
c7b2919
Validate that updated fields are present in the form when creating/up…
kalilsn Apr 22, 2025
810bff2
Fix relation overwrite bug
kalilsn Apr 22, 2025
05289b7
Add form slug to pub update/create calls in tests
kalilsn Apr 22, 2025
b17ba4f
Add label for form switcher to edit form
kalilsn Apr 22, 2025
48c237f
Remove old duplicate authorization check
kalilsn Apr 23, 2025
723bf7b
Fix getAuthorizedUpdateForms query to select editPubWithForm capabili…
kalilsn Apr 23, 2025
2aa371a
Use new capabilties functions on external form pages
kalilsn Apr 23, 2025
89de7c9
Remove unnecessary admin check
kalilsn Apr 23, 2025
d352258
Always use highest role in non-usercan permissions checks
kalilsn Apr 23, 2025
e5b5b13
Try waiting for a second after saving form
kalilsn Apr 23, 2025
94fb175
Wait longer
kalilsn Apr 23, 2025
a9a6bcd
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 6, 2025
e10c343
Fix tests
kalilsn May 6, 2025
23e5c0a
Remove timeout
kalilsn May 6, 2025
804498e
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 7, 2025
607b6ab
Require form and pub pub_type agreement in pub memberships
kalilsn May 7, 2025
cbc4cdc
Add superadmin override to usercan
kalilsn May 12, 2025
070d4b6
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 12, 2025
83dd0ba
Fix some more pubtype/form agreement issues in tests
kalilsn May 12, 2025
c53c390
Factor out member dialog fixture
kalilsn May 12, 2025
9e4205f
Fix mock so usercan doesn't error in db tests
kalilsn May 12, 2025
f24140d
Fix logged in checks
kalilsn May 13, 2025
39c80e6
Limit to authorized types in initial create dialog
kalilsn May 20, 2025
26c9031
Make sure form slug is always set
kalilsn May 20, 2025
6af3884
Remove autocache from base query (not sure why this helps)
kalilsn May 20, 2025
c073b9d
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 20, 2025
fadc96e
Limit create forms by community
kalilsn May 21, 2025
c9af19e
feat: slightly different form switcher (#1246)
tefkah May 28, 2025
3310e67
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 28, 2025
4513507
Use thomas's version of form switcher query management
kalilsn May 28, 2025
42ff32a
Generate pub ids on the server side
kalilsn May 28, 2025
f369d34
Remove unused context
kalilsn May 28, 2025
1684fdc
Fix param order
kalilsn May 28, 2025
4c4efd2
Suppress kysely type error
kalilsn May 28, 2025
96c9f13
Fix update pub action return value
kalilsn May 28, 2025
b6d7b67
Improve create pub button
kalilsn May 29, 2025
70d6898
Merge branch 'main' into kalilsn/form-switcher
kalilsn May 29, 2025
7849305
Fix type error
kalilsn May 31, 2025
695883a
fix: fix e2e test not working
tefkah Jun 2, 2025
761d756
chore: remove color picker
tefkah Jun 2, 2025
395522b
fix: use minio instead of localhost?
tefkah Jun 2, 2025
c622f7f
fix: maybe forceably expose ports?
tefkah Jun 2, 2025
f30ae47
chore: add debug info to see where error happens
tefkah Jun 2, 2025
8b6e90f
fix: try prev config again
tefkah Jun 2, 2025
39869b0
fix: other version of minio?
tefkah Jun 2, 2025
7e6c6df
fix: aa
tefkah Jun 2, 2025
7ee38f3
chore: merge
tefkah Jun 12, 2025
7df40a0
chore: merge + fix selector
tefkah Jun 16, 2025
9a466da
fix: fix update test
tefkah Jun 16, 2025
4b7c0bd
fix: point to minio again
tefkah Jun 16, 2025
79d0ad0
fix: point to minio again
tefkah Jun 16, 2025
c1055b8
chore: merge
tefkah Jun 16, 2025
5ef71b8
fix: add timeouts
tefkah Jun 16, 2025
db1ef44
fix: remove timeout
tefkah Jun 16, 2025
6613ea8
fix: use different storage endpoints for server and for client
tefkah Jun 16, 2025
7d5ebcc
fix: make reorder test more reliable by adding timeouts
tefkah Jun 16, 2025
cc5e839
fix: make reorder test more reliable by adding timeouts
tefkah Jun 16, 2025
1f6ff4d
fix: stupid paste test
tefkah Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.docker-compose.dev
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ ASSETS_BUCKET_NAME=assets.v7.pubpub.org
ASSETS_UPLOAD_KEY=pubpubuser
ASSETS_UPLOAD_SECRET_KEY=pubpubpass
ASSETS_REGION=us-east-1
ASSETS_STORAGE_ENDPOINT=http://localhost:9000
# internal endpoint used by backend services running in Docker
ASSETS_STORAGE_ENDPOINT=http://minio:9000
# public endpoint used for signed URLs accessible from browsers
ASSETS_PUBLIC_ENDPOINT=http://localhost:9000

POSTGRES_PORT=54322
POSTGRES_USER=postgres
Expand Down
8 changes: 3 additions & 5 deletions core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ const handler = createNextHandler(
create: async ({ body }) => {
const { authorization, community, lastModifiedBy } = await checkAuthorization({
token: { scope: ApiAccessScope.pub, type: ApiAccessType.write },
// TODO: figure out capability here
// TODO: refactor so we call userCanCreatePub here
cookies: false,
});

Expand Down Expand Up @@ -423,10 +423,8 @@ const handler = createNextHandler(
update: async ({ params, body }) => {
const { user, community, lastModifiedBy } = await checkAuthorization({
token: { scope: ApiAccessScope.pub, type: ApiAccessType.write },
cookies: {
capability: Capabilities.updatePubValues,
target: { type: MembershipType.pub, pubId: params.pubId as PubsId },
},
// TODO: refactor so we call userCanEditPub here
cookies: false,
});

const { exists } = await doesPubExist(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export const RequestLink = ({
}: {
formSlug: string;
token: string;
pubId: PubsId;
pubId?: PubsId;
}) => {
const useRequestLink = useServerAction(actions.inviteUserToForm);
const sendNewFormLink = useServerAction(actions.sendNewFormLink);
const { id: communityId } = useCommunity();

const requestLink = useCallback(async () => {
const link = await useRequestLink({ slug: formSlug, token, pubId, communityId });
const link = await sendNewFormLink({ slug: formSlug, token, pubId, communityId });

if (link && link.error) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { SaveStatus } from "~/app/components/pubs/PubEditor/SaveStatus";
import { db } from "~/kysely/database";
import { getLoginData } from "~/lib/authentication/loginData";
import { getCommunityRole } from "~/lib/authentication/roles";
import { userCanCreatePub, userCanEditPub } from "~/lib/authorization/capabilities";
import { findCommunityBySlug } from "~/lib/server/community";
import { getForm, userHasPermissionToForm } from "~/lib/server/form";
import { getForm } from "~/lib/server/form";
import { getPubsWithRelatedValues } from "~/lib/server/pub";
import { getPubTypesForCommunity } from "~/lib/server/pubtype";
import { capitalize } from "~/lib/string";
import { ExternalFormWrapper } from "./ExternalFormWrapper";
import { RequestLink } from "./RequestLink";
import { handleFormToken } from "./utils";

const NotFound = ({ children }: { children: ReactNode }) => {
return <div className="w-full pt-8 text-center">{children}</div>;
Expand Down Expand Up @@ -163,6 +163,10 @@ export default async function FormPage(props: {
return <NotFound>No form found</NotFound>;
}

if (searchParams.pubId && !pub) {
return <NotFound>Pub not found</NotFound>;
}

if (!user && !session) {
if (form.access === "public") {
// redirect user to signup/login
Expand All @@ -171,14 +175,6 @@ export default async function FormPage(props: {
);
}

const result = await handleFormToken({
params,
searchParams,
onExpired: ({ params, searchParams, result }) => {
return;
},
});

return (
<ExpiredTokenPage
params={params}
Expand All @@ -203,11 +199,19 @@ export default async function FormPage(props: {

// all other roles always have access to the form
if (role === MemberRole.contributor && form.access !== FormAccessType.public) {
const memberHasAccessToForm = await userHasPermissionToForm({
formSlug: params.formSlug,
userId: user.id,
pubId: pub?.id,
});
const memberHasAccessToForm = pub
? await userCanEditPub({
formSlug: params.formSlug,
userId: user.id,
pubId: pub.id,
})
: await userCanCreatePub({
userId: user.id,
communityId: community.id,
// When the form is in create mode (there's no pub), just pass the form's pubType since
// the user doesn't control the type.
pubTypeId: form.pubTypeId,
});

if (!memberHasAccessToForm) {
// TODO: show no access page
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const updateForm = defineServerAction(async function updateForm({
formId: FormsId;
name: string;
}) {
const user = await getLoginData();
const { user } = await getLoginData();

if (!user) {
return {
Expand Down
2 changes: 1 addition & 1 deletion core/app/c/[communitySlug]/forms/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const createForm = defineServerAction(async function createForm(
slug: string,
communityId: CommunitiesId
) {
const user = await getLoginData();
const { user } = await getLoginData();

if (!user) {
return {
Expand Down
4 changes: 2 additions & 2 deletions core/app/c/[communitySlug]/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getPageLoginData } from "~/lib/authentication/loginData";
import { userCan } from "~/lib/authorization/capabilities";
import { compareMemberRoles } from "~/lib/authorization/rolesRanking";
import { findCommunityBySlug } from "~/lib/server/community";
import { getMembershipForms } from "~/lib/server/form";
import { getSimpleForms } from "~/lib/server/form";
import { selectAllCommunityMemberships } from "~/lib/server/member";
import { addMember, createUserWithCommunityMembership } from "./actions";
import { MemberTable } from "./MemberTable";
Expand Down Expand Up @@ -54,7 +54,7 @@ export default async function Page(props: {
const page = parseInt(searchParams.page ?? "1", 10);
const [members, availableForms] = await Promise.all([
selectAllCommunityMemberships({ communityId: community.id }).execute(),
getMembershipForms(),
getSimpleForms(),
]);

if (!members.length && page !== 1) {
Expand Down
17 changes: 7 additions & 10 deletions core/app/c/[communitySlug]/pubs/PubList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,13 @@ const PaginatedPubListInner = async (props: PaginatedPubListProps) => {
<PubsSelectedProvider pubIds={[]}>
{pubs.map((pub) => {
return (
<div key={pub.id}>
<PubCard
key={pub.id}
pub={pub}
communitySlug={communitySlug}
stages={stages}
actionInstances={actions}
/>
<PubSelector pubId={pub.id} />
</div>
<PubCard
key={pub.id}
pub={pub}
communitySlug={communitySlug}
stages={stages}
actionInstances={actions}
/>
);
})}
<FooterPagination
Expand Down
3 changes: 2 additions & 1 deletion core/app/c/[communitySlug]/pubs/PubSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Checkbox } from "ui/checkbox";

import { usePubsSelectedContext } from "./PubsSelectedContext";

export const PubSelector = ({ pubId }: { pubId: PubsId }) => {
export const PubSelector = ({ pubId, className }: { pubId: PubsId; className?: string }) => {
const { isSelected, toggle } = usePubsSelectedContext();

return (
Expand All @@ -14,6 +14,7 @@ export const PubSelector = ({ pubId }: { pubId: PubsId }) => {
onCheckedChange={() => {
toggle(pubId);
}}
className={className}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { ReactNode } from "react";

import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

import type { ProcessedPubWithForm } from "contracts";
import type { PubTypes, Stages } from "db/public";
import { Badge } from "ui/badge";
import { Button } from "ui/button";
import { Checkbox } from "ui/checkbox";
import { DataTableColumnHeader } from "ui/data-table";

import type { FullProcessedPub } from "~/lib/server/pub";
import { DataTable } from "~/app/components/DataTable/DataTable";
import { dateFormatOptions } from "~/lib/dates";
import { getPubTitle } from "~/lib/pubs";
Expand Down Expand Up @@ -93,14 +93,29 @@ const getRelatedPubsColumns = (relatedPubActionsDropdowns: Record<string, ReactN
return relatedPubActionsDropdowns[row.original.id];
},
},
] as const satisfies ColumnDef<FullProcessedPub, unknown>[];
] as const satisfies ColumnDef<
ProcessedPubWithForm<{
withRelatedPubs: true;
withStage: true;
withPubType: true;
withMembers: true;
withStageActionInstances: true;
}>,
unknown
>[];
};

const Table = ({
pubs,
relatedPubActionsDropdowns,
}: {
pubs: FullProcessedPub[];
pubs: ProcessedPubWithForm<{
withRelatedPubs: true;
withStage: true;
withPubType: true;
withMembers: true;
withStageActionInstances: true;
}>[];
relatedPubActionsDropdowns: Record<string, ReactNode>;
}) => {
const columns = getRelatedPubsColumns(relatedPubActionsDropdowns);
Expand All @@ -112,11 +127,29 @@ export const RelatedPubsTable = ({
pub,
relatedPubActionsDropdowns,
}: {
pub: FullProcessedPub;
pub: ProcessedPubWithForm<{
withRelatedPubs: true;
withStage: true;
withPubType: true;
withMembers: true;
withStageActionInstances: true;
}>;
relatedPubActionsDropdowns: Record<string, ReactNode>;
}) => {
const groupedBySlug = useMemo(() => {
const grouped: Record<string, { pub: FullProcessedPub; fieldName: string }[]> = {};
const grouped: Record<
string,
{
pub: ProcessedPubWithForm<{
withRelatedPubs: true;
withStage: true;
withPubType: true;
withMembers: true;
withStageActionInstances: true;
}>;
fieldName: string;
}[]
> = {};
for (const value of pub.values) {
const { relatedPub, fieldSlug, fieldName } = value;
if (relatedPub) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Info } from "ui/icon";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip";

import type { FullProcessedPub } from "~/lib/server";
import type { FullProcessedPubWithForm } from "~/lib/server";
import { PubsRunActionDropDownMenu } from "~/app/components/ActionUI/PubsRunActionDropDownMenu";
import { RelatedPubsTable } from "./RelatedPubsTable";

Expand All @@ -23,7 +23,7 @@ const NoActions = () => {
);
};

const getRelatedPubRunActionsDropdowns = (row: FullProcessedPub) => {
const getRelatedPubRunActionsDropdowns = (row: FullProcessedPubWithForm) => {
return row.stage && row.stage?.actionInstances.length > 0 ? (
<PubsRunActionDropDownMenu
actionInstances={row.stage.actionInstances}
Expand All @@ -36,7 +36,7 @@ const getRelatedPubRunActionsDropdowns = (row: FullProcessedPub) => {
};

type Props = {
pub: FullProcessedPub;
pub: FullProcessedPubWithForm;
};

export const RelatedPubsTableWrapper = async (props: Props) => {
Expand Down
Loading
Loading