Skip to content

Commit 27ac5a3

Browse files
committed
feat: validation within steps
1 parent b56843b commit 27ac5a3

File tree

2 files changed

+108
-30
lines changed

2 files changed

+108
-30
lines changed

packages/web/app/src/components/organization/settings/access-tokens/create-access-token-sheet-content.tsx

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ const DescriptionInputModel = z
4545
const CreateAccessTokenFormModel = z.object({
4646
title: TitleInputModel,
4747
description: DescriptionInputModel,
48-
selectedPermissions: z.array(z.string()),
49-
assignedResources: z.any(),
48+
permissions: z.array(z.string()).min(1, 'Please select at least one permission.'),
5049
});
5150

5251
const CreateAccessTokenSheetContent_OrganizationFragment = graphql(`
@@ -119,16 +118,14 @@ export function CreateAccessTokenSheetContent(
119118
mode: GraphQLSchema.ResourceAssignmentMode.All,
120119
projects: [],
121120
}));
122-
const [selectedPermissionIds, setSelectedPermissionIds] = useState<ReadonlySet<string>>(
123-
() => new Set<string>(),
124-
);
125121

126-
const form = useForm({
122+
const form = useForm<z.TypeOf<typeof CreateAccessTokenFormModel>>({
127123
mode: 'onChange',
128124
resolver: zodResolver(CreateAccessTokenFormModel),
129125
defaultValues: {
130126
title: '',
131127
description: '',
128+
permissions: [],
132129
},
133130
});
134131

@@ -151,7 +148,7 @@ export function CreateAccessTokenSheetContent(
151148
},
152149
title: formValues.title ?? '',
153150
description: formValues.description ?? '',
154-
permissions: Array.from(selectedPermissionIds),
151+
permissions: formValues.permissions,
155152
resources: resourceSlectionToGraphQLSchemaResourceAssignmentInput(resourceSelection),
156153
},
157154
});
@@ -199,6 +196,7 @@ export function CreateAccessTokenSheetContent(
199196
{stepper.switch({
200197
'step-1-general': () => (
201198
<>
199+
<Heading>General</Heading>
202200
<div className="grid w-full max-w-sm items-center gap-1.5">
203201
<Form.FormField
204202
control={form.control}
@@ -239,25 +237,39 @@ export function CreateAccessTokenSheetContent(
239237
</>
240238
),
241239
'step-2-permissions': () => (
242-
<>
243-
<div className="grid w-full items-center gap-1.5">
244-
<Form.FormItem>
245-
<Form.FormLabel>Permissions</Form.FormLabel>
246-
<Form.FormControl>
247-
<PermissionSelector
248-
permissionGroups={
249-
organization.availableOrganizationPermissionGroups
250-
}
251-
selectedPermissionIds={selectedPermissionIds}
252-
onSelectedPermissionsChange={selectedPermissionIds => {
253-
setSelectedPermissionIds(new Set(selectedPermissionIds));
254-
}}
255-
/>
256-
</Form.FormControl>
257-
<Form.FormMessage />
258-
</Form.FormItem>
259-
</div>
260-
</>
240+
<Form.FormField
241+
control={form.control}
242+
name="permissions"
243+
render={() => (
244+
<div className="grid w-full items-center gap-1.5">
245+
<Form.FormItem>
246+
<Form.FormLabel>
247+
<Heading>Permissions</Heading>
248+
</Form.FormLabel>
249+
<Form.FormControl>
250+
<PermissionSelector
251+
permissionGroups={
252+
organization.availableOrganizationPermissionGroups
253+
}
254+
selectedPermissionIds={new Set(form.getValues()['permissions'])}
255+
onSelectedPermissionsChange={selectedPermissionIds => {
256+
form.setValue(
257+
'permissions',
258+
Array.from(selectedPermissionIds),
259+
{
260+
shouldValidate: true,
261+
shouldTouch: true,
262+
shouldDirty: true,
263+
},
264+
);
265+
}}
266+
/>
267+
</Form.FormControl>
268+
<Form.FormMessage />
269+
</Form.FormItem>
270+
</div>
271+
)}
272+
/>
261273
),
262274
'step-3-resources': () => (
263275
<>
@@ -278,16 +290,16 @@ export function CreateAccessTokenSheetContent(
278290
),
279291
'step-4-confirmation': () => (
280292
<>
281-
<Heading>Confirm and create access token</Heading>
293+
<Heading>Confirm and create Access Token</Heading>
282294
<p className="text-muted-foreground text-sm">
283295
Please please review the selected permissions and resources to ensure they
284296
align with your intended access needs.
285297
</p>
286-
{selectedPermissionIds.size === 0 ? (
298+
{form.getValues().permissions.length === 0 ? (
287299
<p className="mt-3">No permissions are selected.</p>
288300
) : (
289301
<SelectedPermissionOverview
290-
activePermissionIds={Array.from(selectedPermissionIds)}
302+
activePermissionIds={form.getValues().permissions}
291303
permissionsGroups={organization.availableOrganizationPermissionGroups}
292304
showOnlyAllowedPermissions
293305
isExpanded
@@ -344,7 +356,44 @@ export function CreateAccessTokenSheetContent(
344356
: 'Create Access Token'}
345357
</Button>
346358
) : (
347-
<Button onClick={stepper.next}>Next</Button>
359+
<Button
360+
onClick={ev => {
361+
if (stepper.current.id === 'step-1-general') {
362+
Promise.all([form.trigger('title'), form.trigger('description')]).then(
363+
([title, description]) => {
364+
if (!title) {
365+
shakeElement(ev);
366+
form.setFocus('title');
367+
return;
368+
}
369+
if (!description) {
370+
shakeElement(ev);
371+
form.setFocus('description');
372+
return;
373+
}
374+
stepper.next();
375+
},
376+
);
377+
}
378+
379+
if (stepper.current.id === 'step-2-permissions') {
380+
form.trigger('permissions').then(permissions => {
381+
if (!permissions) {
382+
shakeElement(ev);
383+
return;
384+
}
385+
386+
stepper.next();
387+
});
388+
}
389+
390+
if (stepper.current.id === 'step-3-resources') {
391+
stepper.next();
392+
}
393+
}}
394+
>
395+
Next
396+
</Button>
348397
)}
349398
</Stepper.StepperControls>
350399
</Sheet.SheetFooter>
@@ -410,3 +459,15 @@ function AcessTokenCreatedConfirmationDialogue(props: {
410459
</AlertDialog.AlertDialog>
411460
);
412461
}
462+
463+
function shakeElement(ev: React.MouseEvent<HTMLElement>) {
464+
const el = ev.target as HTMLElement;
465+
el.classList.add('animate-shake');
466+
el.addEventListener(
467+
'animationend',
468+
() => {
469+
el.classList.remove('animate-shake');
470+
},
471+
{ once: true },
472+
);
473+
}

packages/web/app/tailwind.config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,21 @@ module.exports = {
238238
backgroundPosition: '-200% 0',
239239
},
240240
},
241+
/** @source https://gist.github.com/krishaantechnology/245b29cfbb25eb456c09fce63673decc */
242+
shake: {
243+
'10%, 90%': {
244+
transform: 'translate3d(-1px, 0, 0)',
245+
},
246+
'20%, 80%': {
247+
transform: 'translate3d(2px, 0, 0)',
248+
},
249+
'30%, 50%, 70%': {
250+
transform: 'translate3d(-4px, 0, 0)',
251+
},
252+
'40%, 60%': {
253+
transform: 'translate3d(4px, 0, 0)',
254+
},
255+
},
241256
},
242257
animation: {
243258
// Dropdown menu
@@ -268,6 +283,8 @@ module.exports = {
268283
'accordion-up': 'accordion-up 0.2s ease-out',
269284
//
270285
shimmer: 'shimmer 1.5s linear infinite',
286+
/** @source https://gist.github.com/krishaantechnology/245b29cfbb25eb456c09fce63673decc */
287+
shake: 'shake 0.82s cubic-bezier(.36,.07,.19,.97) both',
271288
},
272289
minHeight: {
273290
content: 'var(--content-height)',

0 commit comments

Comments
 (0)