Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cms/sveltekit/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
PUBLIC_DIRECTUS_URL=http://localhost:8055
PUBLIC_DIRECTUS_TOKEN=STATIC_TOKEN_FROM_Webmaster_account
DIRECTUS_SERVER_TOKEN=STATIC_TOKEN_FROM_Webmaster_account
PUBLIC_SITE_URL=http://localhost:3000
PUBLIC_DIRECTUS_FORM_TOKEN=STATIC_TOKEN_FROM_Frontend_Bot_User_account
DRAFT_MODE_SECRET=your-draft-mode-secret
PUBLIC_ENABLE_VISUAL_EDITING=true
7 changes: 3 additions & 4 deletions cms/sveltekit/components.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/globals.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
"registry": "https://tw3.shadcn-svelte.com/registry/default"
}
6 changes: 3 additions & 3 deletions cms/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
"@eslint/compat": "^1.3.2",
"@eslint/js": "^9.33.0",
"@sveltejs/enhanced-img": "^0.6.1",
"@sveltejs/kit": "^2.36.1",
"@sveltejs/kit": "^2.49.0",
"@sveltejs/vite-plugin-svelte": "^6.1.3",
"@types/node": "^22.17.2",
"autoprefixer": "^10.4.21",
"bits-ui": "^2.9.4",
"bits-ui": "^1.8.0",
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downgrading bits-ui from ^2.9.4 to ^1.8.0 is a major version downgrade. This could introduce breaking changes or remove features that may be in use elsewhere in the codebase. Ensure this downgrade is intentional and that all components using bits-ui are compatible with version 1.8.0.

Suggested change
"bits-ui": "^1.8.0",
"bits-ui": "^2.9.4",

Copilot uses AI. Check for mistakes.
"clsx": "^2.1.1",
"directus-sdk-typegen": "^0.2.1",
"eslint": "^9.33.0",
Expand All @@ -33,7 +33,7 @@
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.38.2",
"svelte": "^5.44.0",
"svelte-check": "^4.3.1",
"sveltekit-superforms": "^2.27.1",
"tailwind-merge": "^2.6.0",
Expand Down
196 changes: 92 additions & 104 deletions cms/sveltekit/pnpm-lock.yaml

Large diffs are not rendered by default.

131 changes: 82 additions & 49 deletions cms/sveltekit/src/lib/components/forms/DynamicForm.svelte
Original file line number Diff line number Diff line change
@@ -1,75 +1,108 @@
<script lang="ts">
import { dev } from '$app/environment';
import { enhance, applyAction } from '$app/forms';
import { goto } from '$app/navigation';
import setAttr from '$lib/directus/visualEditing';
import type { FormField as FormFieldType } from '$lib/types/directus-schema';
import { buildZodSchema } from '$lib/zodSchemaBuilder';
import Button from '../blocks/Button.svelte';
import type { FormBuilderProps } from './formBuilderTypes';
import Field from './FormField.svelte';
import { superForm, superValidate } from 'sveltekit-superforms';
import SuperDebug from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms';
import { zodClient, zod } from 'sveltekit-superforms/adapters';
import { zodClient } from 'sveltekit-superforms/adapters';
const {
form: formProp,
onSubmitted,
onError
}: FormBuilderProps & { onSubmitted: () => void; onError: () => void } = $props();
interface DynamicFormProps {
fields: FormFieldType[];
onSubmit: (data: Record<string, any>) => void;
submitLabel: string;
id: string;
}
const fields = $derived(formProp.fields);
const submitLabel = $derived(formProp.submit_label);
const id = $derived(formProp.id);
const { fields, onSubmit, submitLabel, id }: DynamicFormProps = $props();
const sortedFields = $derived([...fields].sort((a, b) => (a.sort || 0) - (b.sort || 0)));
const formSchema = $derived(buildZodSchema(fields));
const sortedFields = [...fields].sort((a, b) => (a.sort || 0) - (b.sort || 0));
const formSchema = buildZodSchema(fields);
const defaultValues = $derived(
fields.reduce<Record<string, any>>((defaults, field) => {
if (!field.name) return defaults;
switch (field.type) {
case 'checkbox':
defaults[field.name] = false;
break;
case 'checkbox_group':
defaults[field.name] = [];
break;
case 'radio':
defaults[field.name] = '';
break;
default:
defaults[field.name] = '';
break;
}
const defaultValues = fields.reduce<Record<string, any>>((defaults, field) => {
if (!field.name) return defaults;
switch (field.type) {
case 'checkbox':
defaults[field.name] = false;
break;
case 'checkbox_group':
defaults[field.name] = [];
break;
case 'radio':
defaults[field.name] = '';
break;
default:
defaults[field.name] = '';
break;
}
return defaults;
}, {});
return defaults;
}, {})
);
const form = superForm(defaultValues, {
validators: zodClient(formSchema),
SPA: true
});
const form = $derived(
superForm(defaultValues, {
validators: zodClient(formSchema),
SPA: true
})
);
const { enhance, submit, form: formData, errors, validateForm } = $derived(form);
const { form: formData, errors, validateForm } = $derived(form);
</script>

const onsubmit = async (e: Event) => {
e.preventDefault();
// const f = await superValidate($formData, zod(formSchema));
<form
enctype="multipart/form-data"
class="flex flex-wrap gap-4"
method="POST"
action={`/?/createFormSubmission`}
use:enhance={async ({ formElement, formData, action, cancel, submitter }) => {
// `formElement` is this `<form>` element
// `formData` is its `FormData` object that's about to be submitted
// `action` is the URL to which the form is posted
// calling `cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted
const f = await validateForm();
$errors = f.errors;
if (f.valid) {
onSubmit($formData);
if (!f.valid) {
console.error('Form is not valid', f);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This console.error statement should be removed before merging to production or replaced with proper error handling/logging.

Copilot uses AI. Check for mistakes.
onError();
cancel();
}
};
</script>

<form
class="flex flex-wrap gap-4"
{onsubmit}
return async ({ result }) => {
console.log('formProp', formProp);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This console.log statement should be removed before merging to production. It can expose form configuration data and clutters production logs.

Suggested change
console.log('formProp', formProp);

Copilot uses AI. Check for mistakes.
// `result` is an `ActionResult` object
if (formProp.on_success === 'redirect' && formProp.success_redirect_url) {
if (formProp.success_redirect_url.startsWith('/')) {
goto(formProp.success_redirect_url);
} else {
window.location.href = formProp.success_redirect_url; // TODO check if internal or external
}
} else if (result.type === 'failure') {
onError();
cancel();
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cancel() function is called after onError() on line 87, but this doesn't prevent form submission since the form has already been processed (result type is 'failure'). The cancel() call here has no effect. Consider removing it or restructuring the error handling logic.

Suggested change
cancel();

Copilot uses AI. Check for mistakes.
console.error('result is 400', result);
Comment on lines +87 to +88
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console.error on line 88 should be removed or replaced with proper error handling before production. Additionally, the cancel() call on line 87 has no effect since this is in the return callback after the form has already been submitted.

Suggested change
cancel();
console.error('result is 400', result);

Copilot uses AI. Check for mistakes.
} else {
applyAction(result);
onSubmitted();
}
// `update` is a function which triggers the default logic that would be triggered if this callback wasn't set
};
}}
data-directus={setAttr({
collection: 'forms',
item: id,
fields: 'fields',
mode: 'popover'
})}
>
<!-- add a hidden field for the form id -->
<input type="hidden" name="formId" value={id} />

{#each sortedFields as field (field.id)}
<Field {field} {form} />
{/each}
Expand All @@ -88,7 +121,7 @@
icon="arrow"
label={submitLabel}
iconPosition="right"
id={`submit-${submitLabel.replace(/\s+/g, '-').toLowerCase()}`}
id={`submit-${submitLabel?.replace(/\s+/g, '-').toLowerCase()}`}
></Button>
</div>
</div>
Expand Down
50 changes: 4 additions & 46 deletions cms/sveltekit/src/lib/components/forms/FormBuilder.svelte
Original file line number Diff line number Diff line change
@@ -1,54 +1,13 @@
<script lang="ts">
import { submitForm } from '$lib/directus/forms';
import type { FormField } from '$lib/types/directus-schema';
import { cn } from '$lib/utils';
import { CheckCircle } from '@lucide/svelte';
import DynamicForm from './DynamicForm.svelte';
import { goto } from '$app/navigation';

interface FormBuilderProps {
class?: string;
form: {
id: string;
on_success?: 'redirect' | 'message' | null;
sort?: number | null;
submit_label?: string;
success_message?: string | null;
title?: string | null;
success_redirect_url?: string | null;
is_active?: boolean | null;
fields: FormField[];
};
}
import type { FormBuilderProps } from './formBuilderTypes';

const { form, class: className }: FormBuilderProps = $props();

let isSubmitted = $state(false);
let error = $state<string | null>(null);

const handleSubmit = async (data: Record<string, any>) => {
try {
const fieldsWithNames = form.fields.map((field) => ({
id: field.id,
name: field.name || '',
type: field.type || ''
}));
await submitForm(form.id, fieldsWithNames, data);

if (form.on_success === 'redirect' && form.success_redirect_url) {
if (form.success_redirect_url.startsWith('/')) {
goto(form.success_redirect_url);
} else {
window.location.href = form.success_redirect_url; // TODO check if internal or external
}
} else {
isSubmitted = true;
}
} catch (err) {
console.error('Error submitting form:', err);
error = 'Failed to submit the form. Please try again later.';
}
};
</script>

{#if form.is_active}
Expand All @@ -68,10 +27,9 @@
</div>
{/if}
<DynamicForm
fields={form.fields}
onSubmit={handleSubmit}
submitLabel={form.submit_label || 'Submit'}
id={form.id}
{form}
onSubmitted={() => (isSubmitted = true)}
onError={() => (error = 'Failed to submit the form. Please try again later.')}
/>
</div>
{/if}
Expand Down
20 changes: 11 additions & 9 deletions cms/sveltekit/src/lib/components/forms/FormField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -74,42 +74,44 @@
<Input
{...props}
placeholder={field.placeholder || ''}
name={field.name || ''}
name={fieldName}
bind:value={$formData[field.name!]}
type={field.validation?.includes('email') ? 'email' : 'text'}
/>
{:else if field.type === 'textarea'}
<Textarea
{...props}
placeholder={field.placeholder || ''}
name={field.name || ''}
bind:value={$formData[field.name!]}
name={fieldName}
bind:value={$formData[fieldName!]}
Comment on lines 78 to +86
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent field name variable usage. Line 86 uses $formData[fieldName!] while line 78 uses $formData[field.name!]. For consistency and correctness, all instances should use fieldName (defined at the top of the component) rather than field.name.

Copilot uses AI. Check for mistakes.
required={field.required}
/>
{:else if field.type === 'checkbox'}
<div class="flex items-center space-x-3">
<Checkbox
{...props}
name={field.name}
bind:checked={$formData[field.name!]}
name={fieldName}
bind:checked={$formData[fieldName!]}
required={!!field.required}
/>
<Label for={field.name}>{field.label}</Label>
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The for attribute should reference fieldName instead of field.name for consistency with the rest of the component where fieldName is used (e.g., lines 77, 85, 93).

Suggested change
<Label for={field.name}>{field.label}</Label>
<Label for={fieldName}>{field.label}</Label>

Copilot uses AI. Check for mistakes.
</div>
{:else if field.type === 'checkbox_group'}
<CheckBoxGroup name={field.name || ''} options={field.choices || []} {form} />
<CheckBoxGroup name={fieldName} options={field.choices || []} {form} />
{:else if field.type === 'select'}
<SelectField name={field.name || ''} options={field.choices || []} {form} />
<SelectField name={fieldName} options={field.choices || []} {form} />
{:else if field.type === 'radio'}
<RadioGroup name={field.name || ''} options={field.choices || []} {form} />
<RadioGroup name={fieldName} options={field.choices || []} {form} />
{:else if field.type === 'file'}
<FileUploadField name={field.name || ''} {form} />
<FileUploadField name={fieldName} {form} />
{:else}
<p>Unknown field type: {field.type}</p>
{/if}
{/snippet}
</Form.Control>
<Form.Description>{field.help}</Form.Description>

<!-- When this renders, it causes the form fields to be out of line -->
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment here is misleading. The issue isn't specifically related to form fields being "out of line" - this is the standard location for error messages in a form. Consider updating this comment to be more specific about what issue you're referring to, or remove it if the issue has been resolved.

Suggested change
<!-- When this renders, it causes the form fields to be out of line -->

Copilot uses AI. Check for mistakes.
{#if $errors[fieldName]}
<Form.FieldErrors>
{#each $errors[fieldName] as string[] as error}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
};
</script>

<Input type="file" id={name} onchange={onFileChange} />
<Input type="file" id={name} {name} onchange={onFileChange} required={!!formData.required} />
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The required attribute references formData.required but formData is the form data store (from destructuring form.form), not the field configuration. This will always be falsy since form data doesn't have a required property. The field's required value should be passed as a prop to this component.

Copilot uses AI. Check for mistakes.
16 changes: 16 additions & 0 deletions cms/sveltekit/src/lib/components/forms/formBuilderTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { FormField } from "$lib/types/directus-schema";

export interface FormBuilderProps {
class?: string;
form: {
id: string;
on_success?: 'redirect' | 'message' | null;
sort?: number | null;
submit_label?: string;
success_message?: string | null;
title?: string | null;
success_redirect_url?: string | null;
is_active?: boolean | null;
fields: FormField[];
};
}
26 changes: 2 additions & 24 deletions cms/sveltekit/src/lib/components/ui/input/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
import Root from './input.svelte';

export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
import Root from "./input.svelte";

export {
Root,
//
Root as Input
Root as Input,
};
Loading