Skip to content

Commit 4e5e9a3

Browse files
authored
Feat/finish helper enrollment (#967)
2 parents d2a75fd + 69859e3 commit 4e5e9a3

File tree

72 files changed

+2505
-903
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2505
-903
lines changed

src/app/(payload)/admin/importMap.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config/environment-variables.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from 'zod';
33

44
export const environmentVariables = createEnv({
55
/*
6-
* Serverside Environment variables, not available on the client.
6+
* Server-side environment variables, not available on the client.
77
* Will throw if you access these variables on the client.
88
*/
99
server: {
@@ -20,7 +20,6 @@ export const environmentVariables = createEnv({
2020
HITOBITO_BASE_URL: z.string().url(),
2121
HITOBITO_FORWARD_URL: z.string().url(),
2222
API_TOKEN: z.string().default(''),
23-
BROWSER_COOKIE: z.string().default(''),
2423
HELPER_GROUP: z.string().optional(),
2524
EVENT_ID: z.string().optional(),
2625
GROUPS_WITH_API_ACCESS: z.string().transform((value) =>

src/features/next-auth/utils/next-auth-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ async function refreshAccessToken(token: JWT): Promise<JWT> {
221221
email: profile.email,
222222
name: profile.first_name + ' ' + profile.last_name,
223223
nickname: profile.nickname,
224+
firstName: profile.first_name,
225+
lastName: profile.last_name,
224226
};
225227
} else {
226228
console.error('Failed to refetch user profile after token refresh');
@@ -303,6 +305,8 @@ export const authOptions: NextAuthConfig = {
303305
cevi_db_uuid: token.cevi_db_uuid,
304306
group_ids: token.group_ids,
305307
nickname: token.nickname,
308+
firstName: token.firstName,
309+
lastName: token.lastName,
306310
};
307311
return session;
308312
},
@@ -333,6 +337,8 @@ export const authOptions: NextAuthConfig = {
333337
email: profile.email,
334338
name: profile.first_name + ' ' + profile.last_name,
335339
nickname: profile.nickname,
340+
firstName: profile.first_name,
341+
lastName: profile.last_name,
336342
};
337343
}
338344

src/features/payload-cms/components/form/actions/get-jobs.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,9 @@ export const getJobs = async (
4747
const currentSubmissionsCount = await payload.count({
4848
collection: 'form-submissions',
4949
where: {
50-
or: [
51-
{
52-
'helper-job': {
53-
equals: job.id,
54-
},
55-
},
56-
{
57-
'helper-jobs': {
58-
contains: job.id,
59-
},
60-
},
61-
],
50+
'helper-jobs': {
51+
contains: job.id,
52+
},
6253
},
6354
});
6455
availableQuota = Math.max(0, job.maxQuota - currentSubmissionsCount.totalDocs);

src/features/payload-cms/components/form/cevi-db-login.tsx

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ import {
66
loggedInAsText,
77
loginWithCeviDatabaseText,
88
} from '@/features/payload-cms/components/form/static-form-texts';
9+
import { getFormStorageKey } from '@/features/payload-cms/components/form/utils/get-form-storage-key';
910
import type { StaticTranslationString } from '@/types/types';
1011
import { i18nConfig, type Locale } from '@/types/types';
1112
import { cn } from '@/utils/tailwindcss-override';
1213
import { signIn, signOut, useSession } from 'next-auth/react';
1314
import { useCurrentLocale } from 'next-i18n-router/client';
1415
import React, { useEffect } from 'react';
16+
import type { FieldError, FieldErrorsImpl, FieldValues, Merge } from 'react-hook-form';
1517
import { useFormContext } from 'react-hook-form';
1618

1719
interface CeviDatabaseLoginProperties {
1820
name: string;
1921
label?: string;
2022
saveField?: 'name' | 'uuid' | 'email' | 'nickname';
23+
fieldMapping?: { jwtField: string; formField: string }[];
2124
formId?: string;
2225
required?: boolean;
2326
currentStepIndex?: number;
27+
error?: FieldError | Merge<FieldError, FieldErrorsImpl<FieldValues>>;
2428
}
2529

2630
const loginRequiredMessage: StaticTranslationString = {
@@ -34,25 +38,22 @@ export const CeviDatabaseLogin: React.FC<CeviDatabaseLoginProperties> = ({
3438
label,
3539
required,
3640
saveField,
41+
fieldMapping,
3742
formId,
3843
currentStepIndex,
44+
error,
3945
}) => {
40-
const {
41-
register,
42-
setValue,
43-
getValues,
44-
formState: { errors },
45-
} = useFormContext();
46+
const { register, setValue, getValues } = useFormContext();
4647
const { data: session } = useSession();
4748
const currentLocale = useCurrentLocale(i18nConfig);
4849
const locale = (currentLocale ?? 'en') as Locale;
4950

5051
const handleLogin = (): void => {
5152
const values = getValues();
5253
if (typeof formId === 'string' && formId !== '') {
53-
sessionStorage.setItem(`form-state-${formId}`, JSON.stringify(values));
54+
sessionStorage.setItem(getFormStorageKey(formId, 'state'), JSON.stringify(values));
5455
if (currentStepIndex !== undefined) {
55-
sessionStorage.setItem(`form_step_${formId}`, String(currentStepIndex));
56+
sessionStorage.setItem(getFormStorageKey(formId, 'step'), String(currentStepIndex));
5657
}
5758
}
5859
// Inform browser to replace the current history entry so the back button skips the login trigger
@@ -71,9 +72,9 @@ export const CeviDatabaseLogin: React.FC<CeviDatabaseLoginProperties> = ({
7172
const handleChangeUser = (): void => {
7273
const values = getValues();
7374
if (typeof formId === 'string' && formId !== '') {
74-
sessionStorage.setItem(`form-state-${formId}`, JSON.stringify(values));
75+
sessionStorage.setItem(getFormStorageKey(formId, 'state'), JSON.stringify(values));
7576
if (currentStepIndex !== undefined) {
76-
sessionStorage.setItem(`form_step_${formId}`, String(currentStepIndex));
77+
sessionStorage.setItem(getFormStorageKey(formId, 'step'), String(currentStepIndex));
7778
}
7879
}
7980
const callbackUrl = typeof globalThis === 'undefined' ? undefined : globalThis.location.href;
@@ -86,6 +87,8 @@ export const CeviDatabaseLogin: React.FC<CeviDatabaseLoginProperties> = ({
8687
});
8788
};
8889

90+
const fieldMappingString = fieldMapping ? JSON.stringify(fieldMapping) : undefined;
91+
8992
useEffect(() => {
9093
if (session?.user) {
9194
let valueToSave: string | number | undefined | null;
@@ -108,10 +111,78 @@ export const CeviDatabaseLogin: React.FC<CeviDatabaseLoginProperties> = ({
108111
}
109112
}
110113
setValue(name, valueToSave ?? '', { shouldValidate: true });
114+
115+
const parsedFieldMapping = fieldMappingString
116+
? (JSON.parse(fieldMappingString) as { jwtField: string; formField: string }[])
117+
: undefined;
118+
119+
if (parsedFieldMapping && parsedFieldMapping.length > 0) {
120+
let didPrefill = false;
121+
for (const { jwtField, formField } of parsedFieldMapping) {
122+
let jwtValue: string | number | null | undefined;
123+
switch (jwtField) {
124+
case 'name': {
125+
jwtValue = session.user.name;
126+
break;
127+
}
128+
case 'firstName': {
129+
jwtValue = session.user.firstName;
130+
break;
131+
}
132+
case 'lastName': {
133+
jwtValue = session.user.lastName;
134+
break;
135+
}
136+
case 'email': {
137+
jwtValue = session.user.email;
138+
break;
139+
}
140+
case 'nickname': {
141+
jwtValue = session.user.nickname;
142+
break;
143+
}
144+
case 'uuid': {
145+
jwtValue = session.user.uuid;
146+
break;
147+
}
148+
case 'cevi_db_uuid': {
149+
jwtValue = session.user.cevi_db_uuid;
150+
break;
151+
}
152+
default: {
153+
break;
154+
}
155+
}
156+
const jwtValueString =
157+
jwtValue !== undefined && jwtValue !== null ? String(jwtValue).trim() : '';
158+
159+
if (jwtValueString.length > 0) {
160+
const currentValue = getValues(formField) as unknown;
161+
let currentValueString = '';
162+
if (typeof currentValue === 'string' || typeof currentValue === 'number') {
163+
currentValueString = String(currentValue).trim();
164+
} else if (currentValue !== undefined && currentValue !== null) {
165+
currentValueString = 'has_value';
166+
}
167+
168+
if (currentValueString.length === 0) {
169+
setValue(formField, String(jwtValue), { shouldValidate: true });
170+
didPrefill = true;
171+
}
172+
}
173+
}
174+
175+
if (didPrefill && typeof formId === 'string' && currentStepIndex !== undefined) {
176+
sessionStorage.setItem(
177+
getFormStorageKey(formId, 'prefill'),
178+
String(currentStepIndex + 1),
179+
);
180+
}
181+
}
111182
}
112-
}, [session, saveField, setValue, name]);
183+
}, [session, saveField, setValue, name, fieldMappingString, getValues, formId, currentStepIndex]);
113184

114-
const errorMessage = errors[name]?.message as string | undefined;
185+
const errorMessage = error ? (error as FieldError).message : undefined;
115186

116187
const nickname = session?.user.nickname;
117188
const hasNickname = typeof nickname === 'string' && nickname.length > 0;

src/features/payload-cms/components/form/checkbox.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@ import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical
77
import { useCurrentLocale } from 'next-i18n-router/client';
88
import type { CheckboxField } from 'payload';
99
import type React from 'react';
10-
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form';
10+
import type {
11+
FieldError,
12+
FieldErrorsImpl,
13+
FieldValues,
14+
Merge,
15+
UseFormRegister,
16+
} from 'react-hook-form';
1117

1218
export const Checkbox: React.FC<
1319
{
14-
errors: Partial<
15-
FieldErrorsImpl<{
16-
[x: string]: never;
17-
}>
18-
>;
20+
error?: FieldError | Merge<FieldError, FieldErrorsImpl<FieldValues>>;
1921
registerAction: UseFormRegister<string & FieldValues>;
2022
label: SerializedEditorState;
2123
} & CheckboxField
22-
> = ({ name, label, registerAction, required: requiredFromProperties, errors }) => {
24+
> = ({ name, label, registerAction, required: requiredFromProperties, error }) => {
2325
// set default values
2426
requiredFromProperties ??= false;
25-
const hasError = errors[name];
27+
const hasError = error !== undefined;
2628
const locale = useCurrentLocale(i18nConfig);
2729

2830
return (
@@ -38,14 +40,16 @@ export const Checkbox: React.FC<
3840
/>
3941

4042
<label
41-
className="ml-2 font-['Inter'] text-sm font-medium text-balance text-gray-500 hover:text-gray-900 [&_div]:inline [&_p]:inline"
43+
className="ml-2 min-w-0 flex-1 font-['Inter'] text-sm font-medium text-balance break-words text-gray-500 hover:text-gray-900 [&_div]:inline [&_p]:inline"
4244
htmlFor={name}
4345
>
4446
<LexicalRichTextSection richTextSection={label} locale={locale as Locale} />
4547
{requiredFromProperties && <Required />}
4648
</label>
4749
</div>
48-
{hasError && <p className="mt-1 text-xs text-red-600">{hasError.message as string}</p>}
50+
{hasError && (
51+
<p className="mt-1 text-xs text-red-600">{(error as { message?: string }).message}</p>
52+
)}
4953
</div>
5054
);
5155
};

src/features/payload-cms/components/form/components/form-field-renderer.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
JobSelectionBlock,
88
} from '@/features/payload-cms/components/form/types';
99
import React, { useEffect } from 'react';
10-
import { useFormContext, useWatch } from 'react-hook-form';
10+
import { useFormContext, useFormState, useWatch } from 'react-hook-form';
1111

1212
interface FormFieldRendererProperties {
1313
section: FormSection;
@@ -36,7 +36,7 @@ const ConditionedField: React.FC<{
3636
useEffect(() => {
3737
if (!isVisible) {
3838
for (const f of block.fields) {
39-
if ('name' in f && f.name) {
39+
if ('name' in f && typeof f.name === 'string' && f.name !== '') {
4040
resetField(f.name);
4141
}
4242
}
@@ -49,7 +49,11 @@ const ConditionedField: React.FC<{
4949
<>
5050
{block.fields.map((field, index) => (
5151
<SingleField
52-
key={('id' in field && typeof field.id === 'string' ? field.id : undefined) || index}
52+
key={
53+
('id' in field && typeof field.id === 'string' && field.id !== ''
54+
? field.id
55+
: undefined) ?? index
56+
}
5357
field={field}
5458
currentStepIndex={currentStepIndex}
5559
formId={formId}
@@ -67,16 +71,15 @@ const SingleField: React.FC<{
6771
renderMode: 'all' | 'sidebar' | 'main';
6872
}> = ({ field, currentStepIndex, formId, renderMode }) => {
6973
const Component = fieldComponents[field.blockType];
70-
const {
71-
register,
72-
control,
73-
formState: { errors },
74-
} = useFormContext();
74+
const { register, control } = useFormContext();
7575

7676
const fieldName = 'name' in field && typeof field.name === 'string' ? field.name : undefined;
7777

78-
// Force React Hook Form to track the error for this specific field by reading the proxy
79-
void (fieldName ? Boolean(errors[fieldName]) : false);
78+
const { errors } = useFormState({
79+
name: fieldName ?? '',
80+
});
81+
82+
const fieldError = fieldName !== undefined && fieldName !== '' ? errors[fieldName] : undefined;
8083

8184
if (!Component) {
8285
console.error(`Field type ${field.blockType} is not supported`);
@@ -96,7 +99,7 @@ const SingleField: React.FC<{
9699
{...field}
97100
registerAction={register}
98101
control={control}
99-
errors={errors}
102+
error={fieldError}
100103
required={field.required}
101104
currentStepIndex={currentStepIndex}
102105
formId={formId}
@@ -113,15 +116,19 @@ export const FormFieldRenderer: React.FC<FormFieldRendererProperties> = ({
113116
}) => {
114117
return (
115118
<div className="space-y-4">
116-
{renderMode !== 'main' && section.sectionTitle && (
117-
<SubheadingH3 className="mt-0">{section.sectionTitle}</SubheadingH3>
118-
)}
119+
{renderMode !== 'main' &&
120+
typeof section.sectionTitle === 'string' &&
121+
section.sectionTitle !== '' && (
122+
<SubheadingH3 className="mt-0">{section.sectionTitle}</SubheadingH3>
123+
)}
119124

120125
{section.fields.map((field, index) => {
121126
if (field.blockType === 'conditionedBlock') {
122127
return (
123128
<ConditionedField
124-
key={field.id || index}
129+
key={
130+
(typeof field.id === 'string' && field.id !== '' ? field.id : undefined) ?? index
131+
}
125132
block={field}
126133
currentStepIndex={currentStepIndex}
127134
formId={formId}
@@ -131,7 +138,11 @@ export const FormFieldRenderer: React.FC<FormFieldRendererProperties> = ({
131138
}
132139
return (
133140
<SingleField
134-
key={('id' in field && typeof field.id === 'string' ? field.id : undefined) || index}
141+
key={
142+
('id' in field && typeof field.id === 'string' && field.id !== ''
143+
? field.id
144+
: undefined) ?? index
145+
}
135146
field={field}
136147
currentStepIndex={currentStepIndex}
137148
formId={formId}

0 commit comments

Comments
 (0)