Skip to content

Commit 022e2c0

Browse files
hobbescodescoopbri
andauthored
Project Page Mutations (#58)
* build: add necessary form dependencies * refactor(dashboard): enable new project cta * feature(dashboard): set up base for new project dialog * feature(dashboard): set up base for new project dialog * refactor(new-project): add select component for organization selection * refactor(new-project): move select organization label to app.config * feature(new-project): add necessary form inputs * feature(new-project): apply form field validation schema * refactor(new-project): update schema validation for project slug * feature(graphql): add create project mutation * refactor(graphql): update organizations query * feature(dashboard): add proper mutation for creating a new project * refactor(new-project): add async validation for project slug * fix(build): update queries to fix build errors due to merge conflicts * refactor: update routing to use org and proj slugs * fix(new-project): update create porject payload to include org slug * feature(dashboard): add dialog for creating a new organization * feature(dashboard): add functionality for creating an organization * refactor(dashboard): update responsive design pattern for pinned orgs * refactor(dashboard): adjust state management for CTAs when there are no user orgs * chore: format * refactor: adjust pattern for form submissions * chore: format * refactor: adjust pattern for FormFieldError * docs: add JSDoc to custom Link component * chore: format * fix: update field level validator for organization name * build: update to use JSONC formatted lockfile * feature(organizations): apply logic to handle mutations * refactor: update auth flow to inject database rowId into the session user object * chore: format * chore: add TODO comment regarding augmentation of session object * chore: update comment related to onChangeAsync validator Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * chore: update comment regarding onChangeAsync validator Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * refactor(components): use custom link component throughout app * refactor(config): update placeholder text for form inputs * fix(link): extend props to include html anchor element attributes * refactor(link): remove unnecessary children prop override * refactor: update slug validations, use custom validation adapter * feature(organization): add appropriate create project mutations * refactor(providers): adjust query client prover implementation * chore: format * refactor: adjust graphqlFetch to allow for properly exposed fetchers * fix: remove unused export * feature(dashboard): prefetch pinned organizations * chore: format * refactor(organizations): prefetch query, use nuqs search params server side * chore: format * refactor(search-params): use cache pattern for future proofing * feature(organization): fetch projects server side and hydrate client * fix: update organizations page to query orgs from currently signed in user * refactor: update early return / bail pattern for async RSCs * fix: update enabled variable for queries that require specifics from the user * refactor: remove unused return from mutation * refactor: adjust comments and TODOs, remove web3 dependencies * refactor: adjust naming convention for schema validator import, update comment * refactor: adjust casing for schema validator import * chore: update comments * feature: turbo or bust * refactor: manage states based on isLoading from useAuth hook * refactor: update prefetch patterns to include multiple queries * refactor(projects): add prefetch pattern * feature(project-page): add prefetching pattern * refactor: update dialog state management to use zustand factory pattern * refactor(hooks): extract useDialogStore options interface * refactor(prefetch-pattern): use Promise.all rather than Promise.allSettled * build(deps): update dependencies * feature(feedback): set up prefetching pattern for dynamic feedback page * fix(feedback): establish hydration boundary * chore(layout): updating casing for comment Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * refactor(create-project): extract custom mutation key * chore: format * refactor(account-info): adjust styles for menu * refactor(project): extract feedback form into separate component * chore: format * refactor(project): update form to use labels, restructure placeholders * chore: format * feature(project): add create feedback functionality WIP * refactor(constants): adjust mutation keys to use generated keys from codegen * chore: format * feature(graphql): add create upvote / downvote mutations * feature(graphql): add create upvote / downvote mutations * feature(feedback): add upvote / downvote functionality * feature(feedback): toggle upvotes / downvotes for user * refactor(feedback): strengthen optimistic update pattern * refactor(feedback): allow removal of individual upvotes or downvotes * chore: format * refactor(feedback-metrics): optimistically update total engagement * refactor: optimistally update total responses and total feedback counts * chore: format * refactor: add default orderBy for posts query * chore: format * feature(project): optimistically update the project feedback upon submit * fix(feedback): update FeedbackDetails to dynamically determine how feedback is queried * refactor(create-feedback): simplify disabled state for create button * refactor(dashboard): update new organization CTA variant for consistency across app * fix(project-overview): update props to remove incorrect typing of projectId * refactor(feedback-details): remove unecessary conditional href * refactor(create-feedback): update schema error messages * chore: format * refactor: update mutations and queries for voting to avoid confusion with id vs rowId * chore(query-provider): add comment regarding change to global invalidation * fix: typo * chore(mutation-keys): add additional imports Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * fix(mutation-keys): update delete upvote Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * fix(mutation-keys): update delete downvote Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * chore(codegen): update comment regarding exposeMutationKeys setting Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * refactor(config): update placeholder copy Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com> * refactor: remove optimistic updates for project metrics, conditionally render vote buttons * refactor(feedback-details): update pattern for isPending * refactor(project-overview): adjust type reference for projectId prop --------- Co-authored-by: Brian Cooper <20056195+coopbri@users.noreply.github.com>
1 parent 44e43e1 commit 022e2c0

File tree

25 files changed

+1121
-383
lines changed

25 files changed

+1121
-383
lines changed

src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const ProjectPage = async ({ params }: Props) => {
100100
label: app.projectPage.header.cta.settings.label,
101101
// TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331
102102
icon: <LuSettings />,
103+
disabled: true,
103104
},
104105
{
105106
label: app.projectPage.header.cta.viewAllProjects.label,

src/components/dashboard/DashboardPage/DashboardPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const DashboardPage = () => {
7676
// TODO: get Sigil Icon component working and update accordingly. Context: https://github.com/omnidotdev/backfeed-app/pull/44#discussion_r1897974331
7777
icon: <LuPlusCircle />,
7878
dialogType: DialogType.CreateOrganization,
79-
variant: "muted",
79+
variant: "outline",
8080
},
8181
{
8282
label: app.dashboardPage.cta.newProject.label,
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"use client";
2+
3+
import {
4+
Button,
5+
Input,
6+
Label,
7+
Skeleton,
8+
Stack,
9+
Text,
10+
Textarea,
11+
sigil,
12+
} from "@omnidev/sigil";
13+
import { useForm } from "@tanstack/react-form";
14+
import { useQueryClient } from "@tanstack/react-query";
15+
import { useParams } from "next/navigation";
16+
import { z } from "zod";
17+
18+
import { FormFieldError } from "components/core";
19+
import {
20+
useCreateFeedbackMutation,
21+
useInfinitePostsQuery,
22+
useProjectQuery,
23+
} from "generated/graphql";
24+
import { app } from "lib/config";
25+
import {
26+
CREATE_FEEDBACK_MUTATION_KEY,
27+
standardSchemaValidator,
28+
} from "lib/constants";
29+
import { useAuth } from "lib/hooks";
30+
31+
// TODO adjust schema in this file after closure on https://linear.app/omnidev/issue/OMNI-166/strategize-runtime-and-server-side-validation-approach and https://linear.app/omnidev/issue/OMNI-167/refine-validation-schemas
32+
33+
/** Schema for defining the shape of the create feedback form fields, as well as validating the form. */
34+
const createFeedbackSchema = z.object({
35+
projectId: z
36+
.string()
37+
.uuid(app.projectPage.projectFeedback.createFeedback.errors.invalid),
38+
userId: z
39+
.string()
40+
.uuid(app.projectPage.projectFeedback.createFeedback.errors.invalid),
41+
title: z
42+
.string()
43+
.min(3, app.projectPage.projectFeedback.createFeedback.errors.title),
44+
description: z
45+
.string()
46+
.min(10, app.projectPage.projectFeedback.createFeedback.errors.description),
47+
});
48+
49+
interface Props {
50+
/** Loading state for current feedback. */
51+
isLoading: boolean;
52+
/** Error state for current feedback. */
53+
isError: boolean;
54+
/** Total feedback for the project. */
55+
totalCount: number;
56+
}
57+
58+
/**
59+
* Create feedback form.
60+
*/
61+
const CreateFeedback = ({ isLoading, isError, totalCount }: Props) => {
62+
const queryClient = useQueryClient();
63+
64+
const { organizationSlug, projectSlug } = useParams<{
65+
organizationSlug: string;
66+
projectSlug: string;
67+
}>();
68+
69+
const { user, isLoading: isAuthLoading } = useAuth();
70+
71+
const { data: projectId, isLoading: isProjectLoading } = useProjectQuery(
72+
{
73+
projectSlug,
74+
organizationSlug,
75+
},
76+
{
77+
enabled: !!projectSlug && !!organizationSlug,
78+
select: (data) => data?.projects?.nodes?.[0]?.rowId,
79+
}
80+
);
81+
82+
const { mutate: createFeedback, isPending } = useCreateFeedbackMutation({
83+
mutationKey: CREATE_FEEDBACK_MUTATION_KEY,
84+
onSuccess: () => {
85+
reset();
86+
87+
return queryClient.invalidateQueries(
88+
{
89+
queryKey: useInfinitePostsQuery.getKey({
90+
pageSize: 5,
91+
projectId: projectId!,
92+
}),
93+
},
94+
{ cancelRefetch: false }
95+
);
96+
},
97+
});
98+
99+
const { handleSubmit, Field, Subscribe, reset } = useForm({
100+
defaultValues: {
101+
projectId: projectId ?? "",
102+
userId: user?.rowId ?? "",
103+
title: "",
104+
description: "",
105+
},
106+
asyncDebounceMs: 300,
107+
validatorAdapter: standardSchemaValidator,
108+
validators: {
109+
onMount: createFeedbackSchema,
110+
onChangeAsync: createFeedbackSchema,
111+
},
112+
onSubmit: ({ value }) =>
113+
createFeedback({
114+
input: {
115+
post: {
116+
projectId: value.projectId,
117+
userId: value.userId,
118+
title: value.title,
119+
description: value.description,
120+
},
121+
},
122+
}),
123+
});
124+
125+
const isFormDisabled = isProjectLoading || isAuthLoading;
126+
127+
return (
128+
<sigil.form
129+
display="flex"
130+
flexDirection="column"
131+
gap={4}
132+
onSubmit={(e) => {
133+
e.preventDefault();
134+
e.stopPropagation();
135+
handleSubmit();
136+
}}
137+
>
138+
<Field
139+
name="title"
140+
validators={{
141+
onBlur: createFeedbackSchema.shape.title,
142+
}}
143+
>
144+
{({ handleChange, handleBlur, state }) => (
145+
<Stack position="relative" gap={1.5}>
146+
<Label htmlFor="title">
147+
{app.projectPage.projectFeedback.feedbackTitle.label}
148+
</Label>
149+
150+
<Input
151+
id="title"
152+
placeholder={
153+
app.projectPage.projectFeedback.feedbackTitle.placeholder
154+
}
155+
borderColor="border.subtle"
156+
value={state.value}
157+
onChange={(e) => handleChange(e.target.value)}
158+
onBlur={handleBlur}
159+
disabled={isFormDisabled}
160+
/>
161+
162+
<FormFieldError
163+
error={state.meta.errorMap.onBlur}
164+
isDirty={state.meta.isDirty}
165+
/>
166+
</Stack>
167+
)}
168+
</Field>
169+
170+
<Field
171+
name="description"
172+
validators={{
173+
onBlur: createFeedbackSchema.shape.description,
174+
}}
175+
>
176+
{({ handleChange, handleBlur, state }) => (
177+
<Stack position="relative" gap={1.5}>
178+
<Label htmlFor="description">
179+
{app.projectPage.projectFeedback.feedbackDescription.label}
180+
</Label>
181+
182+
<Textarea
183+
id="description"
184+
placeholder={
185+
app.projectPage.projectFeedback.feedbackDescription.placeholder
186+
}
187+
borderColor="border.subtle"
188+
rows={5}
189+
minH={32}
190+
value={state.value}
191+
onChange={(e) => handleChange(e.target.value)}
192+
onBlur={handleBlur}
193+
disabled={isFormDisabled}
194+
/>
195+
196+
<FormFieldError
197+
error={state.meta.errorMap.onBlur}
198+
isDirty={state.meta.isDirty}
199+
/>
200+
</Stack>
201+
)}
202+
</Field>
203+
204+
<Stack justify="space-between" direction="row">
205+
<Skeleton isLoaded={!isLoading} h="fit-content">
206+
<Text
207+
fontSize="sm"
208+
color="foreground.muted"
209+
>{`${isError ? 0 : totalCount} ${app.projectPage.projectFeedback.totalResponses}`}</Text>
210+
</Skeleton>
211+
212+
<Subscribe
213+
selector={(state) => [
214+
state.canSubmit,
215+
state.isSubmitting,
216+
state.isDirty,
217+
]}
218+
>
219+
{([canSubmit, isSubmitting, isDirty]) => (
220+
<Button
221+
type="submit"
222+
w="fit-content"
223+
placeSelf="flex-end"
224+
disabled={!canSubmit || !isDirty || isPending}
225+
>
226+
{isSubmitting || isPending
227+
? app.projectPage.projectFeedback.action.pending
228+
: app.projectPage.projectFeedback.action.submit}
229+
</Button>
230+
)}
231+
</Subscribe>
232+
</Stack>
233+
</sigil.form>
234+
);
235+
};
236+
237+
export default CreateFeedback;

0 commit comments

Comments
 (0)