Skip to content

Commit 08f8b6f

Browse files
committed
Add form switcher
1 parent f3c9d0e commit 08f8b6f

File tree

11 files changed

+306
-31
lines changed

11 files changed

+306
-31
lines changed

core/app/c/[communitySlug]/members/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getPageLoginData } from "~/lib/authentication/loginData";
1010
import { userCan } from "~/lib/authorization/capabilities";
1111
import { firstRoleIsHigher } from "~/lib/authorization/rolesRanking";
1212
import { findCommunityBySlug } from "~/lib/server/community";
13-
import { getMembershipForms } from "~/lib/server/form";
13+
import { getSimpleForms } from "~/lib/server/form";
1414
import { selectAllCommunityMemberships } from "~/lib/server/member";
1515
import { addMember, createUserWithCommunityMembership } from "./actions";
1616
import { MemberTable } from "./MemberTable";
@@ -54,7 +54,7 @@ export default async function Page(props: {
5454
const page = parseInt(searchParams.page ?? "1", 10);
5555
const [members, availableForms] = await Promise.all([
5656
selectAllCommunityMemberships({ communityId: community.id }).execute(),
57-
getMembershipForms(),
57+
getSimpleForms(),
5858
]);
5959

6060
if (!members.length && page !== 1) {

core/app/c/[communitySlug]/pubs/[pubId]/edit/page.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ export default async function Page(props: {
113113
return null;
114114
}
115115

116-
const formId = `edit-pub-${pub.id}`;
116+
const htmlFormId = `edit-pub-${pub.id}`;
117117

118118
return (
119119
<ContentLayout
120120
left={
121-
<Button form={formId} type="submit">
121+
<Button form={htmlFormId} type="submit">
122122
Save
123123
</Button>
124124
}
@@ -135,7 +135,7 @@ export default async function Page(props: {
135135
<PubEditor
136136
searchParams={searchParams}
137137
pubId={pub.id}
138-
formId={formId}
138+
htmlFormId={htmlFormId}
139139
communityId={community.id}
140140
/>
141141
</div>

core/app/c/[communitySlug]/pubs/[pubId]/page.tsx

+16-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Pencil } from "ui/icon";
1111
import Move from "~/app/c/[communitySlug]/stages/components/Move";
1212
import { MembersList } from "~/app/components//Memberships/MembersList";
1313
import { PubsRunActionDropDownMenu } from "~/app/components/ActionUI/PubsRunActionDropDownMenu";
14+
import { FormSwitcher } from "~/app/components/FormSwitcher/FormSwitcher";
1415
import { AddMemberDialog } from "~/app/components/Memberships/AddMemberDialog";
1516
import { CreatePubButton } from "~/app/components/pubs/CreatePubButton";
1617
import { RemovePubButton } from "~/app/components/pubs/RemovePubButton";
@@ -22,7 +23,7 @@ import { getPubByForm, getPubTitle } from "~/lib/pubs";
2223
import { getPubsWithRelatedValues, pubValuesByVal } from "~/lib/server";
2324
import { autoCache } from "~/lib/server/cache/autoCache";
2425
import { findCommunityBySlug } from "~/lib/server/community";
25-
import { getForm, getMembershipForms } from "~/lib/server/form";
26+
import { getForm, getSimpleForms } from "~/lib/server/form";
2627
import { selectAllCommunityMemberships } from "~/lib/server/member";
2728
import { getStages } from "~/lib/server/stages";
2829
import {
@@ -68,7 +69,7 @@ export default async function Page(props: {
6869
params: Promise<{ pubId: PubsId; communitySlug: string }>;
6970
searchParams: Promise<Record<string, string>>;
7071
}) {
71-
const searchParams = await props.searchParams;
72+
const { form: formSlug, ...searchParams } = await props.searchParams;
7273
const params = await props.params;
7374
const { pubId, communitySlug } = params;
7475

@@ -137,24 +138,26 @@ export default async function Page(props: {
137138
}
138139

139140
const actionsPromise = pub.stage ? getStageActions(pub.stage.id).execute() : null;
140-
141+
const getFormProps = formSlug
142+
? { communityId: community.id, slug: formSlug }
143+
: {
144+
communityId: community.id,
145+
pubTypeId: pub.pubType.id,
146+
};
141147
const [actions, communityMembers, communityStages, form, withExtraPubValues, availableForms] =
142148
await Promise.all([
143149
actionsPromise,
144150
communityMembersPromise,
145151
communityStagesPromise,
146-
getForm({
147-
communityId: community.id,
148-
pubTypeId: pub.pubType.id,
149-
}).executeTakeFirstOrThrow(
152+
getForm(getFormProps).executeTakeFirstOrThrow(
150153
() => new Error(`Could not find a form for pubtype ${pub.pubType.name}`)
151154
),
152155
userCan(
153156
Capabilities.seeExtraPubValues,
154157
{ type: MembershipType.pub, pubId: pub.id },
155158
user.id
156159
),
157-
getMembershipForms(pub.pubType.id),
160+
getSimpleForms(pub.pubType.id),
158161
]);
159162

160163
const pubTypeHasRelatedPubs = pub.pubType.fields.some((field) => field.isRelation);
@@ -170,8 +173,11 @@ export default async function Page(props: {
170173
<div className="flex flex-col space-y-4">
171174
<div className="mb-8 flex items-center justify-between">
172175
<div>
173-
<div className="text-lg font-semibold text-muted-foreground">
174-
{pub.pubType.name}
176+
<div className="flex items-center gap-2">
177+
<span className="text-lg font-semibold text-muted-foreground">
178+
{pub.pubType.name}
179+
</span>
180+
<FormSwitcher defaultFormSlug={formSlug} forms={availableForms} />
175181
</div>
176182
<h1 className="mb-2 text-xl font-bold">{getPubTitle(pub)} </h1>
177183
</div>

core/app/c/[communitySlug]/pubs/create/page.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Metadata } from "next";
22

33
import { notFound, redirect } from "next/navigation";
44

5-
import type { CommunitiesId } from "db/public";
65
import { Capabilities, MembershipType } from "db/public";
76
import { Button } from "ui/button";
87

@@ -57,12 +56,12 @@ export default async function Page(props: {
5756
redirect(`/c/${communitySlug}/unauthorized`);
5857
}
5958

60-
const formId = `create-pub`;
59+
const htmlFormId = `create-pub`;
6160

6261
return (
6362
<ContentLayout
6463
left={
65-
<Button form={formId} type="submit">
64+
<Button form={htmlFormId} type="submit">
6665
Save
6766
</Button>
6867
}
@@ -74,7 +73,7 @@ export default async function Page(props: {
7473
<PubEditor
7574
searchParams={searchParams}
7675
communityId={community.id}
77-
formId={formId}
76+
htmlFormId={htmlFormId}
7877
// PubEditor checks for the existence of the stageId prop
7978
{...(searchParams["stageId"] ? { stageId: searchParams["stageId"] } : {})}
8079
/>

core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelMembers.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { AddMemberDialog } from "~/app/components/Memberships/AddMemberDialog";
1212
import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard";
1313
import { userCan } from "~/lib/authorization/capabilities";
1414
import { getStageMembers } from "~/lib/db/queries";
15-
import { getMembershipForms } from "~/lib/server/form";
15+
import { getSimpleForms } from "~/lib/server/form";
1616
import {
1717
addStageMember,
1818
addUserWithStageMembership,
@@ -29,7 +29,7 @@ const StagePanelMembersInner = async ({ stageId, user }: PropsInner) => {
2929
const [members, canManage, availableForms] = await Promise.all([
3030
getStageMembers(stageId).execute(),
3131
userCan(Capabilities.removeStageMember, { type: MembershipType.stage, stageId }, user.id),
32-
getMembershipForms(),
32+
getSimpleForms(),
3333
]);
3434

3535
return (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
5+
6+
import { Button } from "ui/button";
7+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";
8+
9+
import type { SimpleForm } from "~/lib/server/form";
10+
11+
export const formSwitcherUrlParam = "form";
12+
13+
export const FormSwitcher = ({
14+
forms,
15+
defaultFormSlug,
16+
}: {
17+
forms: SimpleForm[];
18+
defaultFormSlug?: string;
19+
}) => {
20+
const router = useRouter();
21+
const pathname = usePathname();
22+
const params = useSearchParams();
23+
24+
const [selectedFormSlug, setSelectedFormSlug] = useState(defaultFormSlug ?? forms[0].slug);
25+
if (!forms.length) {
26+
return null;
27+
}
28+
29+
const switchForms = () => {
30+
if (!selectedFormSlug) {
31+
return;
32+
}
33+
const newParams = new URLSearchParams(params);
34+
newParams.set(formSwitcherUrlParam, selectedFormSlug);
35+
router.push(`${pathname}?${newParams.toString()}`);
36+
};
37+
38+
return (
39+
<div className="flex items-center gap-2">
40+
<Select
41+
onValueChange={(slug: string) => {
42+
setSelectedFormSlug(slug);
43+
}}
44+
defaultValue={selectedFormSlug}
45+
>
46+
<SelectTrigger className="">
47+
<SelectValue />
48+
</SelectTrigger>
49+
<SelectContent>
50+
{forms.map((form) => (
51+
<SelectItem key={form.id} value={form.slug}>
52+
{form.name}
53+
</SelectItem>
54+
))}
55+
</SelectContent>
56+
</Select>
57+
<Button onClick={switchForms}>Switch forms</Button>
58+
</div>
59+
);
60+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
4+
5+
import { createContext, useContext, useMemo, useState } from "react";
6+
7+
import type { Form } from "~/lib/server/form";
8+
9+
export type FormSwitcherContext = {
10+
selectedForm?: Form;
11+
setSelectedForm: Dispatch<SetStateAction<Form | undefined>>;
12+
};
13+
14+
const FormSwitcherContext = createContext<FormSwitcherContext>({
15+
setSelectedForm: () => {},
16+
});
17+
18+
type Props = PropsWithChildren<{
19+
defaultForm?: Form;
20+
}>;
21+
22+
export const FormSwitcherProvider = ({ defaultForm, children }: Props) => {
23+
const [selectedForm, setSelectedForm] = useState(defaultForm);
24+
25+
const value = useMemo(
26+
() => ({ selectedForm, setSelectedForm }),
27+
[selectedForm, setSelectedForm]
28+
);
29+
30+
return <FormSwitcherContext.Provider value={value}>{children}</FormSwitcherContext.Provider>;
31+
};
32+
33+
export const useFormSwitcherContext = () => useContext(FormSwitcherContext);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { skipToken } from "@tanstack/react-query";
5+
6+
import type { FormsId } from "db/public";
7+
import { Button } from "ui/button";
8+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";
9+
import { toast } from "ui/use-toast";
10+
11+
import type { SimpleForm } from "~/lib/server/form";
12+
import { client } from "~/lib/api";
13+
import { useCommunity } from "../providers/CommunityProvider";
14+
import { useFormSwitcherContext } from "./FormSwitcherContext";
15+
16+
const FormSwitcher = ({ forms }: { forms: SimpleForm[] }) => {
17+
const community = useCommunity();
18+
const { selectedForm, setSelectedForm } = useFormSwitcherContext();
19+
const [selectedFormId, setSelectedFormId] = useState(selectedForm?.id);
20+
const { isLoading, refetch } = client.forms.get.useQuery({
21+
enabled: false,
22+
queryKey: ["getForm", selectedFormId, community.slug],
23+
queryData: selectedFormId
24+
? { params: { formId: selectedFormId, communitySlug: community.slug } }
25+
: skipToken,
26+
select(data) {
27+
if (data.body) {
28+
setSelectedForm(data.body);
29+
}
30+
},
31+
throwOnError(error, query) {
32+
toast({
33+
title: "Error",
34+
description: `Unable to switch forms`,
35+
variant: "destructive",
36+
});
37+
return false;
38+
},
39+
});
40+
41+
if (!forms.length) {
42+
return null;
43+
}
44+
45+
const switchForms = () => {
46+
refetch();
47+
};
48+
49+
return (
50+
<div>
51+
<Select
52+
onValueChange={(value: FormsId) => {
53+
setSelectedFormId(value);
54+
}}
55+
disabled={isLoading}
56+
>
57+
<SelectTrigger className="">
58+
<SelectValue placeholder="Form" />
59+
</SelectTrigger>
60+
<SelectContent>
61+
{forms.map((form) => (
62+
<SelectItem key={form.id} value={form.id}>
63+
{form.name}
64+
</SelectItem>
65+
))}
66+
</SelectContent>
67+
</Select>
68+
<Button onClick={switchForms}>Switch forms</Button>
69+
</div>
70+
);
71+
};

core/app/components/Memberships/MemberInviteForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export const MemberInviteForm = ({
262262
</FormItem>
263263
)}
264264
/>
265-
{isContributor && (
265+
{isContributor && !!availableForms.length && (
266266
<FormField
267267
control={form.control}
268268
name="forms"

core/app/components/pubs/PubEditor/PubEditor.tsx

+10-6
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ const getRelatedPubData = async ({
104104

105105
export type PubEditorProps = {
106106
searchParams: { relatedPubId?: PubsId; slug?: string; pubTypeId?: PubTypesId };
107-
formId?: string;
107+
htmlFormId?: string;
108+
formSlug?: string;
108109
} & (
109110
| {
110111
pubId: PubsId;
@@ -210,10 +211,13 @@ export async function PubEditor(props: PubEditorProps) {
210211
);
211212
}
212213

213-
const form = await getForm({
214-
communityId: community.id,
215-
pubTypeId: pubType.id,
216-
}).executeTakeFirstOrThrow(
214+
const getFormProps = props.formSlug
215+
? { communityId: community.id, slug: props.formSlug }
216+
: {
217+
communityId: community.id,
218+
pubTypeId: pubType.id,
219+
};
220+
const form = await getForm(getFormProps).executeTakeFirstOrThrow(
217221
() => new Error(`Could not find a form for pubtype ${pubType.name}`)
218222
);
219223

@@ -290,7 +294,7 @@ export async function PubEditor(props: PubEditorProps) {
290294
isUpdating={isUpdating}
291295
withAutoSave={false}
292296
withButtonElements
293-
htmlFormId={props.formId}
297+
htmlFormId={props.htmlFormId}
294298
stageId={currentStageId}
295299
relatedPub={
296300
relatedPubId && relatedFieldSlug

0 commit comments

Comments
 (0)