Skip to content

Commit b45e8da

Browse files
thiessenp-cdstimarneyanikbrazeau
authored
feat: my forms update (#7240)
* initial layout changes * Fix an alignment issue * misc style updates * minor css tweaks * css updates * more style updates * add initial logic to poll edit-lock * polling updates * optimize the authorization check for polling * polling fixes * fix a collaborator count bug * clean up the card logic * style cleanup for crads * Update polling to only poll when the tab is active * update shared to recently edited * add edit link to menu for draft forms * add inifinite scrolling * bump the types package version * update create form button * add account to nav * Add edit lock message * Update collaborator wording * card cleanup * add settings link to published cards * add card list optimizations * add some polling optimizations * big cards refactor for better maintainability * movign cards from server to client folder * itterating on some performance improvements * more perf fixes * add co-editing help component * design updates * fixed a few archive related bugs * fix another archived bug * update colors * update content formatting * update card links and lastEditedBy * misc cleanup * more misc fixes * update the page title with the current active tab * update page title with sub title from tag * update card to have title link first in tab order * remove old comments * update comments * add transitions to the cards list * add underline to card titles on hover * fix card archive color * chore: bump yarn to 4.16.0 (#7296) bump yarn * chore: skip captcha for local dev (#7297) * add skip * revert some header styles * fix menu keyboard open * update to always show recently edited button * add ga event for when a user clicks resume editing * minor optimization for ga callback * add progressive backoff for polling * update progressive backup times * move newFormButton into the list view * updates new form to show on published tab as well * update coments * create icons out of inlined icons * update draft edit link to no longer show a dialog * update draftEditLink to use a link * Update i18n/translations/en/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/en/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/en/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * Update i18n/translations/fr/my-forms.json Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com> * fix a merge error * update card layout --------- Co-authored-by: Tim Arney <Tim Arney> Co-authored-by: Tim Arney <timarney@users.noreply.github.com> Co-authored-by: Anik Brazeau <38330843+anikbrazeau@users.noreply.github.com>
1 parent 21ccff5 commit b45e8da

39 files changed

Lines changed: 1729 additions & 425 deletions

File tree

app/(gcforms)/[locale]/(form administration)/form-builder/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import {
2222
updateFormSaveAndResume,
2323
getFormJSONConfig,
2424
updateFormBranding,
25+
getFullTemplateByID,
2526
} from "@lib/templates";
2627
import { serverTranslation } from "@i18n";
2728
import { revalidatePath } from "next/cache";
2829
import { isValidDateString } from "@lib/utils/date/isValidDateString";
2930
import { allowedTemplates, TemplateTypes } from "@lib/utils/form-builder";
30-
import { getFullTemplateByID } from "@lib/templates";
3131
import { isValidEmail } from "@gcforms/core";
3232
import { slugify } from "@lib/client/clientHelpers";
3333
import { sendEmail } from "@lib/integration/notifyConnector";

app/(gcforms)/[locale]/(form administration)/forms/components/Invitations/Invitations.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const Invitations = ({ invitations }: { invitations: Omit<Invitation, "in
4343
);
4444
};
4545

46+
if (!invitations || invitations.length === 0) {
47+
return null;
48+
}
49+
4650
return (
4751
<div className="mb-4 flex flex-col gap-2">
4852
{invitations.map((invitation: Omit<Invitation, "invitedBy">) => {

app/(gcforms)/[locale]/(form administration)/forms/components/ResumeEditingForm.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"use client";
2-
import React, { useEffect, useRef } from "react";
2+
import React, { useCallback, useEffect, useRef } from "react";
33
import Link from "next/link";
44
import { useTranslation } from "@i18n/client";
55
import Skeleton from "react-loading-skeleton";
66
import { clearTemplateStore } from "@lib/store/utils";
77
import { safeJSONParse } from "@lib/utils";
88
import { FormServerErrorCodes } from "@lib/types/form-builder-types";
9+
import { ga } from "@root/lib/client/clientHelpers";
910

1011
type FormStateType = {
1112
state: {
@@ -60,11 +61,16 @@ export const ResumeEditingForm = () => {
6061
}
6162
}, []);
6263

64+
const handleResumeClick = useCallback(() => {
65+
ga("resume_editing_form_click", { formId: formIdRef.current });
66+
}, []);
67+
6368
return hasSession ? (
6469
<Link
6570
id={formIdRef.current}
6671
href={`/${i18n.language}/form-builder/${formIdRef.current}/edit`}
6772
className="mb-4 inline-block"
73+
onClick={handleResumeClick}
6874
>
6975
<span aria-hidden="true"></span> {t("actions.resumeForm")}
7076
</Link>
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
"use client";
2+
3+
import { Suspense, useMemo, memo } from "react";
4+
import { EnvelopeIcon, MessageIcon, GearIcon } from "@serverComponents/icons";
5+
import { Menu } from "../client/Menu";
6+
import { Unarchive } from "../client/Unarchive";
7+
import Skeleton from "react-loading-skeleton";
8+
import { DraftEditLink } from "../client/DraftEditLink";
9+
import { useTranslation } from "@i18n/client";
10+
import { useParams } from "next/navigation";
11+
import Link from "next/link";
12+
import { FormsTemplateWithLockInfo } from "../types";
13+
import {
14+
getCardState,
15+
formatDateToYYYYMMDD,
16+
daysUntilTTL,
17+
isTTLWarningPeriod,
18+
calculateCollaboratorCount,
19+
getBannerColor,
20+
} from "../helpers";
21+
22+
const CardBanner = memo(({ isPublished }: { isPublished: boolean }) => {
23+
const { t } = useTranslation("my-forms");
24+
const bulletColor = getBannerColor(isPublished);
25+
26+
return (
27+
<div className="mt-4 flex items-center gap-1 self-start text-sm" aria-hidden="true">
28+
<span
29+
className={`inline-block h-3 w-3 rounded-full border-1 border-slate-500 ${bulletColor}`}
30+
/>
31+
{isPublished ? t("card.states.published") : t("card.states.draft")}
32+
</div>
33+
);
34+
});
35+
CardBanner.displayName = "CardBanner";
36+
37+
const CardLinks = memo(
38+
({
39+
isPublished,
40+
id,
41+
deliveryOption,
42+
overdue,
43+
ttl,
44+
language,
45+
}: {
46+
id: string;
47+
url?: string;
48+
isPublished: boolean;
49+
deliveryOption?: { emailAddress?: string } | null;
50+
overdue: boolean;
51+
ttl?: Date | null;
52+
language: string;
53+
}) => {
54+
const { t } = useTranslation("my-forms");
55+
const responsesLink = `/${language}/form-builder/${id}/responses`;
56+
const settingsLink = `/${language}/form-builder/${id}/settings`;
57+
58+
if (!isPublished) {
59+
return <div className="mb-4" />;
60+
}
61+
62+
return (
63+
<div className="mt-2">
64+
{ttl != null && <Unarchive id={id} isPublished={isPublished} language={language} />}
65+
66+
{/* Email delivery */}
67+
{deliveryOption && deliveryOption.emailAddress && (
68+
<span className="mt-4 block text-sm">
69+
<EnvelopeIcon className="mr-2 inline-block" />
70+
{t("card.deliveryOption.email")} {deliveryOption.emailAddress}
71+
</span>
72+
)}
73+
74+
{/* Vault delivery */}
75+
{deliveryOption && ttl == null && !deliveryOption.emailAddress && (
76+
<>
77+
{overdue ? (
78+
<span className="text-red mt-4 block text-sm">
79+
<MessageIcon className="mr-2 inline-block" />
80+
{t("card.actionRequired.description")} {""}
81+
<a href={responsesLink}>{t("card.actionRequired.linkText")}</a>
82+
</span>
83+
) : (
84+
<Link
85+
className="mt-4 block text-sm focus:fill-slate-500 active:fill-slate-500"
86+
href={responsesLink}
87+
prefetch={false}
88+
>
89+
<MessageIcon className="mr-2 ml-px inline-block" />
90+
{t("card.deliveryOption.vault", { ns: "my-forms" })}{" "}
91+
</Link>
92+
)}
93+
</>
94+
)}
95+
96+
{/* Settings link - only for published non-archived forms */}
97+
{ttl == null && (
98+
<Link
99+
className="mt-2 block text-sm focus:fill-slate-500 active:fill-slate-500"
100+
href={settingsLink}
101+
prefetch={false}
102+
>
103+
<GearIcon className="mr-2 inline-block" />
104+
{t("card.menu.settings")}
105+
</Link>
106+
)}
107+
</div>
108+
);
109+
}
110+
);
111+
CardLinks.displayName = "CardLinks";
112+
113+
const CardTitle = memo(
114+
({
115+
id,
116+
name,
117+
isPublished,
118+
collaboratorCount,
119+
}: {
120+
id: string;
121+
name: string;
122+
isPublished: boolean;
123+
collaboratorCount: number;
124+
}) => {
125+
const {
126+
t,
127+
i18n: { language },
128+
} = useTranslation("my-forms");
129+
const classes =
130+
"mb-0 mr-2 block w-full overflow-hidden pb-0 text-left text-base font-bold line-clamp-3 no-underline text-inherit hover:underline focus:underline active:underline cursor-pointer!";
131+
const publishedLink = `/${language}/form-builder/${id}/responses`;
132+
const draftLink = `/${language}/form-builder/${id}/edit/`;
133+
return isPublished ? (
134+
<Link className={classes} href={publishedLink} prefetch={false}>
135+
{name ? name : t("card.unnamedForm")}
136+
</Link>
137+
) : (
138+
<DraftEditLink
139+
href={draftLink}
140+
formId={id}
141+
className={classes}
142+
collaboratorCount={collaboratorCount}
143+
>
144+
{name ? name : t("card.unnamedForm")}
145+
</DraftEditLink>
146+
);
147+
}
148+
);
149+
CardTitle.displayName = "CardTitle";
150+
151+
const CardDate = memo(({ id, date, ttl }: { id: string; date: string; ttl?: Date | null }) => {
152+
const { t } = useTranslation("my-forms");
153+
154+
const formattedDate = formatDateToYYYYMMDD(date);
155+
const showTTLWarning = ttl ? isTTLWarningPeriod(ttl) : false;
156+
const daysRemaining = ttl ? daysUntilTTL(ttl) : 0;
157+
158+
return (
159+
<div id={`card-date-${id}`} className="mb-1 text-sm">
160+
{t("card.lastEdited")}: {formattedDate}
161+
{ttl && (
162+
<>
163+
<br />
164+
<span>
165+
{t("card.deleteDate")}
166+
{formatDateToYYYYMMDD(ttl)}
167+
</span>
168+
{showTTLWarning && (
169+
<span className="ml-4 text-red-500">
170+
{t("card.deletedIn")} {daysRemaining} {t("card.days")}
171+
</span>
172+
)}
173+
</>
174+
)}
175+
</div>
176+
);
177+
});
178+
CardDate.displayName = "CardDate";
179+
180+
const CardCollaboratorCount = memo(({ collaboratorCount }: { collaboratorCount: number }) => {
181+
const { t } = useTranslation("my-forms");
182+
if (collaboratorCount <= 0) return null;
183+
return <p className="mb-2 pt-2 text-sm">{t("card.sharedWith", { count: collaboratorCount })}</p>;
184+
});
185+
CardCollaboratorCount.displayName = "CardCollaboratorCount";
186+
187+
const CardFooterDraftReadonly = memo(
188+
({
189+
cardId,
190+
date,
191+
ttl,
192+
lastEditedBy,
193+
}: {
194+
cardId: string;
195+
date: string;
196+
ttl?: Date | null;
197+
lastEditedBy?: string | null;
198+
}) => {
199+
const { t } = useTranslation("my-forms");
200+
return (
201+
<div>
202+
<CardDate id={cardId} date={date} ttl={ttl} />
203+
{lastEditedBy && (
204+
<div className="mt-2 text-sm">{t("cards.lastEditedBy", { lastEditedBy })}</div>
205+
)}
206+
</div>
207+
);
208+
}
209+
);
210+
CardFooterDraftReadonly.displayName = "CardFooterDraftReadonly";
211+
212+
const CardFooterDraftEditing = memo(
213+
({
214+
lockedByName,
215+
language,
216+
cardId,
217+
collaboratorCount,
218+
}: {
219+
lockedByName: string | null;
220+
language: string;
221+
cardId: string;
222+
collaboratorCount: number;
223+
}) => {
224+
const { t } = useTranslation("my-forms");
225+
return (
226+
<div>
227+
<div className="mb-2 text-sm">{lockedByName} editing</div>
228+
<div className="flex items-center">
229+
<div className="mr-2 text-sm">Read only -</div>
230+
<DraftEditLink
231+
href={`/${language}/form-builder/${cardId}/edit/`}
232+
formId={cardId}
233+
className="cursor-pointer! text-left text-sm text-inherit underline focus:text-slate-500 active:text-slate-500"
234+
collaboratorCount={collaboratorCount}
235+
>
236+
{t("editForm")}
237+
</DraftEditLink>
238+
</div>
239+
</div>
240+
);
241+
}
242+
);
243+
CardFooterDraftEditing.displayName = "CardFooterDraftEditing";
244+
245+
const CardFooterPublished = memo(({ cardId }: { cardId: string }) => {
246+
return <p className="mt-3 text-xs italic">{cardId}</p>;
247+
});
248+
CardFooterPublished.displayName = "CardFooterPublished";
249+
250+
const CardComponent = ({ card, status }: { card: FormsTemplateWithLockInfo; status?: string }) => {
251+
const params = useParams();
252+
const language = params?.locale as string;
253+
254+
// Calculate collaborator count (excluding the owner)
255+
const collaboratorCount = useMemo(
256+
() =>
257+
calculateCollaboratorCount(
258+
card.collaboratorCount.userCount,
259+
card.collaboratorCount.pendingUserCount
260+
),
261+
[card.collaboratorCount.userCount, card.collaboratorCount.pendingUserCount]
262+
);
263+
264+
const cardState = useMemo(() => getCardState(card), [card]);
265+
266+
const wrapperClass = `grid h-full max-w-[16em] min-w-[16em] grid-cols-[1fr_auto] gap-2 rounded-md border-1 border-slate-300 pt-2 pr-3 pb-4 pl-5 shadow-lg shadow-slate-900/5 ${card.editLockInfo ? "bg-yellow-50" : ""}`;
267+
268+
return (
269+
<div className={wrapperClass} data-testid={`card-${card.id}`}>
270+
<div className="row-start-1 mt-1 flex flex-col">
271+
<CardTitle
272+
id={card.id}
273+
name={card.name}
274+
isPublished={card.isPublished}
275+
collaboratorCount={collaboratorCount}
276+
/>
277+
<CardCollaboratorCount collaboratorCount={collaboratorCount} />
278+
<Suspense fallback={<Skeleton count={2} className="my-3 w-[300px]" />}>
279+
<CardLinks
280+
isPublished={card.isPublished}
281+
url={card.url}
282+
id={card.id}
283+
deliveryOption={card.deliveryOption}
284+
overdue={card.overdue}
285+
ttl={card.ttl}
286+
language={language}
287+
/>
288+
</Suspense>
289+
<div className="mt-auto">
290+
{cardState === "draft-readonly" && (
291+
<CardFooterDraftReadonly
292+
cardId={card.id}
293+
date={card.date}
294+
ttl={card.ttl}
295+
lastEditedBy={card.lastEditedBy}
296+
/>
297+
)}
298+
{cardState === "draft-editing" && (
299+
<CardFooterDraftEditing
300+
lockedByName={card.editLockInfo?.lockedByName ?? null}
301+
language={language}
302+
cardId={card.id}
303+
collaboratorCount={collaboratorCount}
304+
/>
305+
)}
306+
<CardBanner isPublished={card.isPublished} />
307+
{cardState === "published" && <CardFooterPublished cardId={card.id} />}
308+
</div>
309+
</div>
310+
<div className="flex items-start">
311+
<Menu
312+
id={card.id}
313+
name={card.name}
314+
isPublished={card.isPublished}
315+
ttl={card.ttl ? card.ttl : undefined}
316+
status={status}
317+
/>
318+
</div>
319+
</div>
320+
);
321+
};
322+
export const Card = memo(CardComponent);

0 commit comments

Comments
 (0)