refactor(js): use superstruct instead of zod
This commit is contained in:
parent
fea8d8971b
commit
14a1bfa1a3
9 changed files with 201 additions and 152 deletions
|
@ -15,6 +15,7 @@ import {
|
||||||
import { useMemo, useRef, createContext, useContext } from 'react';
|
import { useMemo, useRef, createContext, useContext } from 'react';
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { findOrCreateContainerElement } from '@coldwired/react';
|
import { findOrCreateContainerElement } from '@coldwired/react';
|
||||||
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useLabelledBy,
|
useLabelledBy,
|
||||||
|
@ -98,7 +99,7 @@ export function SingleComboBox({
|
||||||
form,
|
form,
|
||||||
data,
|
data,
|
||||||
...props
|
...props
|
||||||
} = useMemo(() => SingleComboBoxProps.parse(maybeProps), [maybeProps]);
|
} = useMemo(() => s.create(maybeProps, SingleComboBoxProps), [maybeProps]);
|
||||||
|
|
||||||
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
||||||
const { ref, dispatch } = useDispatchChangeEvent();
|
const { ref, dispatch } = useDispatchChangeEvent();
|
||||||
|
@ -146,7 +147,7 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) {
|
||||||
allowsCustomValue,
|
allowsCustomValue,
|
||||||
valueSeparator,
|
valueSeparator,
|
||||||
...props
|
...props
|
||||||
} = useMemo(() => MultiComboBoxProps.parse(maybeProps), [maybeProps]);
|
} = useMemo(() => s.create(maybeProps, MultiComboBoxProps), [maybeProps]);
|
||||||
|
|
||||||
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
||||||
const { ref, dispatch } = useDispatchChangeEvent();
|
const { ref, dispatch } = useDispatchChangeEvent();
|
||||||
|
@ -235,7 +236,7 @@ export function RemoteComboBox({
|
||||||
form,
|
form,
|
||||||
data,
|
data,
|
||||||
...props
|
...props
|
||||||
} = useMemo(() => RemoteComboBoxProps.parse(maybeProps), [maybeProps]);
|
} = useMemo(() => s.create(maybeProps, RemoteComboBoxProps), [maybeProps]);
|
||||||
|
|
||||||
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
const labelledby = useLabelledBy(props.id, ariaLabelledby);
|
||||||
const { ref, dispatch } = useDispatchChangeEvent();
|
const { ref, dispatch } = useDispatchChangeEvent();
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { matchSorter } from 'match-sorter';
|
||||||
import { useDebounceCallback } from 'usehooks-ts';
|
import { useDebounceCallback } from 'usehooks-ts';
|
||||||
import { useEvent } from 'react-use-event-hook';
|
import { useEvent } from 'react-use-event-hook';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
import { Item } from './props';
|
import { Item } from './props';
|
||||||
|
|
||||||
|
@ -420,9 +421,9 @@ export const createLoader: (
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const result = Item.array().safeParse(json);
|
const [err, result] = s.validate(json, s.array(Item), { coerce: true });
|
||||||
if (result.success) {
|
if (!err) {
|
||||||
const items = matchSorter(result.data, filterText, {
|
const items = matchSorter(result, filterText, {
|
||||||
keys: ['label']
|
keys: ['label']
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,72 +1,81 @@
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { z } from 'zod';
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
import type { Loader } from './hooks';
|
import type { Loader } from './hooks';
|
||||||
|
|
||||||
export const Item = z.object({
|
export const Item = s.object({
|
||||||
label: z.string(),
|
label: s.string(),
|
||||||
value: z.string(),
|
value: s.string(),
|
||||||
data: z.any().optional()
|
data: s.any()
|
||||||
});
|
});
|
||||||
export type Item = z.infer<typeof Item>;
|
export type Item = s.Infer<typeof Item>;
|
||||||
|
|
||||||
const ComboBoxPropsSchema = z
|
const ArrayOfTuples = s.coerce(
|
||||||
.object({
|
s.array(Item),
|
||||||
id: z.string(),
|
s.array(s.tuple([s.string(), s.union([s.string(), s.number()])])),
|
||||||
className: z.string(),
|
(items) =>
|
||||||
name: z.string(),
|
items.map<Item>(([label, value]) => ({
|
||||||
label: z.string(),
|
label,
|
||||||
description: z.string(),
|
value: String(value)
|
||||||
isRequired: z.boolean(),
|
}))
|
||||||
'aria-label': z.string(),
|
);
|
||||||
'aria-labelledby': z.string(),
|
|
||||||
'aria-describedby': z.string(),
|
const ArrayOfStrings = s.coerce(s.array(Item), s.array(s.string()), (items) =>
|
||||||
items: z
|
items.map<Item>((label) => ({ label, value: label }))
|
||||||
.array(Item)
|
);
|
||||||
.or(
|
|
||||||
z
|
const ComboBoxPropsSchema = s.partial(
|
||||||
.string()
|
s.object({
|
||||||
.array()
|
id: s.string(),
|
||||||
.transform((items) =>
|
className: s.string(),
|
||||||
items.map<Item>((label) => ({ label, value: label }))
|
name: s.string(),
|
||||||
)
|
label: s.string(),
|
||||||
)
|
description: s.string(),
|
||||||
.or(
|
isRequired: s.boolean(),
|
||||||
z
|
'aria-label': s.string(),
|
||||||
.tuple([z.string(), z.string().or(z.number())])
|
'aria-labelledby': s.string(),
|
||||||
.array()
|
'aria-describedby': s.string(),
|
||||||
.transform((items) =>
|
items: s.union([s.array(Item), ArrayOfStrings, ArrayOfTuples]),
|
||||||
items.map<Item>(([label, value]) => ({
|
formValue: s.enums(['text', 'key']),
|
||||||
label,
|
form: s.string(),
|
||||||
value: String(value)
|
data: s.record(s.string(), s.string())
|
||||||
}))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
formValue: z.enum(['text', 'key']),
|
|
||||||
form: z.string(),
|
|
||||||
data: z.record(z.string())
|
|
||||||
})
|
})
|
||||||
.partial();
|
);
|
||||||
export const SingleComboBoxProps = ComboBoxPropsSchema.extend({
|
export const SingleComboBoxProps = s.assign(
|
||||||
selectedKey: z.string().nullable(),
|
ComboBoxPropsSchema,
|
||||||
emptyFilterKey: z.string()
|
s.partial(
|
||||||
}).partial();
|
s.object({
|
||||||
export const MultiComboBoxProps = ComboBoxPropsSchema.extend({
|
selectedKey: s.nullable(s.string()),
|
||||||
selectedKeys: z.string().array(),
|
emptyFilterKey: s.string()
|
||||||
allowsCustomValue: z.boolean(),
|
})
|
||||||
valueSeparator: z.string()
|
)
|
||||||
}).partial();
|
);
|
||||||
export const RemoteComboBoxProps = ComboBoxPropsSchema.extend({
|
export const MultiComboBoxProps = s.assign(
|
||||||
selectedKey: z.string().nullable(),
|
ComboBoxPropsSchema,
|
||||||
minimumInputLength: z.number(),
|
s.partial(
|
||||||
limit: z.number(),
|
s.object({
|
||||||
allowsCustomValue: z.boolean()
|
selectedKeys: s.array(s.string()),
|
||||||
}).partial();
|
allowsCustomValue: s.boolean(),
|
||||||
export type SingleComboBoxProps = z.infer<typeof SingleComboBoxProps> & {
|
valueSeparator: s.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
export const RemoteComboBoxProps = s.assign(
|
||||||
|
ComboBoxPropsSchema,
|
||||||
|
s.partial(
|
||||||
|
s.object({
|
||||||
|
selectedKey: s.nullable(s.string()),
|
||||||
|
minimumInputLength: s.number(),
|
||||||
|
limit: s.number(),
|
||||||
|
allowsCustomValue: s.boolean()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
export type SingleComboBoxProps = s.Infer<typeof SingleComboBoxProps> & {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
export type MultiComboBoxProps = z.infer<typeof MultiComboBoxProps>;
|
export type MultiComboBoxProps = s.Infer<typeof MultiComboBoxProps>;
|
||||||
export type RemoteComboBoxProps = z.infer<typeof RemoteComboBoxProps> & {
|
export type RemoteComboBoxProps = s.Infer<typeof RemoteComboBoxProps> & {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
loader: Loader | string;
|
loader: Loader | string;
|
||||||
onChange?: (item: Item | null) => void;
|
onChange?: (item: Item | null) => void;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Editor, type JSONContent } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { isButtonElement, isHTMLElement } from '@coldwired/utils';
|
import { isButtonElement, isHTMLElement } from '@coldwired/utils';
|
||||||
import { z } from 'zod';
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
import { ApplicationController } from '../application_controller';
|
import { ApplicationController } from '../application_controller';
|
||||||
import { getAction } from '../../shared/tiptap/actions';
|
import { getAction } from '../../shared/tiptap/actions';
|
||||||
|
@ -61,7 +61,7 @@ export class TiptapController extends ApplicationController {
|
||||||
|
|
||||||
insertTag(event: MouseEvent) {
|
insertTag(event: MouseEvent) {
|
||||||
if (this.#editor && isHTMLElement(event.target)) {
|
if (this.#editor && isHTMLElement(event.target)) {
|
||||||
const tag = tagSchema.parse(event.target.dataset);
|
const tag = s.create(event.target.dataset, tagSchema);
|
||||||
const editor = this.#editor
|
const editor = this.#editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
|
@ -77,12 +77,12 @@ export class TiptapController extends ApplicationController {
|
||||||
private get content() {
|
private get content() {
|
||||||
const value = this.inputTarget.value;
|
const value = this.inputTarget.value;
|
||||||
if (value) {
|
if (value) {
|
||||||
return jsonContentSchema.parse(JSON.parse(value));
|
return s.create(JSON.parse(value), jsonContentSchema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get tags(): TagSchema[] {
|
private get tags(): TagSchema[] {
|
||||||
return this.tagTargets.map((tag) => tagSchema.parse(tag.dataset));
|
return this.tagTargets.map((tag) => s.create(tag.dataset, tagSchema));
|
||||||
}
|
}
|
||||||
|
|
||||||
private get menuButtons() {
|
private get menuButtons() {
|
||||||
|
@ -92,13 +92,24 @@ export class TiptapController extends ApplicationController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonContentSchema: z.ZodType<JSONContent> = z.object({
|
const Attrs = s.record(s.string(), s.any());
|
||||||
type: z.string().optional(),
|
const Marks = s.array(
|
||||||
text: z.string().optional(),
|
s.type({
|
||||||
attrs: z.record(z.any()).optional(),
|
type: s.string(),
|
||||||
marks: z
|
attrs: s.optional(Attrs)
|
||||||
.object({ type: z.string(), attrs: z.record(z.any()).optional() })
|
})
|
||||||
.array()
|
);
|
||||||
.optional(),
|
type JSONContent = {
|
||||||
content: z.lazy(() => z.array(jsonContentSchema).optional())
|
type?: string;
|
||||||
|
text?: string;
|
||||||
|
attrs?: s.Infer<typeof Attrs>;
|
||||||
|
marks?: s.Infer<typeof Marks>;
|
||||||
|
content?: JSONContent[];
|
||||||
|
};
|
||||||
|
const jsonContentSchema: s.Describe<JSONContent> = s.type({
|
||||||
|
type: s.optional(s.string()),
|
||||||
|
text: s.optional(s.string()),
|
||||||
|
attrs: s.optional(Attrs),
|
||||||
|
marks: s.optional(Marks),
|
||||||
|
content: s.lazy(() => s.optional(s.array(jsonContentSchema)))
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { z } from 'zod';
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
type EditorAction = {
|
type EditorAction = {
|
||||||
run(): void;
|
run(): void;
|
||||||
|
@ -11,7 +11,7 @@ export function getAction(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
button: HTMLButtonElement
|
button: HTMLButtonElement
|
||||||
): EditorAction {
|
): EditorAction {
|
||||||
return tiptapActionSchema.parse(button.dataset)(editor);
|
return s.create(button.dataset, tiptapActionSchema)(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
|
const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
|
||||||
|
@ -109,8 +109,15 @@ const EDITOR_ACTIONS: Record<string, (editor: Editor) => EditorAction> = {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const tiptapActionSchema = z
|
const EditorActionFn = s.define<(editor: Editor) => EditorAction>(
|
||||||
.object({
|
'EditorActionFn',
|
||||||
tiptapAction: z.enum(Object.keys(EDITOR_ACTIONS) as [string, ...string[]])
|
(fn) => typeof fn == 'function'
|
||||||
})
|
);
|
||||||
.transform(({ tiptapAction }) => EDITOR_ACTIONS[tiptapAction]);
|
|
||||||
|
const tiptapActionSchema = s.coerce(
|
||||||
|
EditorActionFn,
|
||||||
|
s.type({
|
||||||
|
tiptapAction: s.enums(Object.keys(EDITOR_ACTIONS) as [string, ...string[]])
|
||||||
|
}),
|
||||||
|
({ tiptapAction }) => EDITOR_ACTIONS[tiptapAction]
|
||||||
|
);
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion';
|
import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion';
|
||||||
import { z } from 'zod';
|
import * as s from 'superstruct';
|
||||||
import tippy, { type Instance as TippyInstance } from 'tippy.js';
|
import tippy, { type Instance as TippyInstance } from 'tippy.js';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
|
|
||||||
export const tagSchema = z
|
export const tagSchema = s.coerce(
|
||||||
.object({ tagLabel: z.string(), tagId: z.string() })
|
s.object({ label: s.string(), id: s.string() }),
|
||||||
.transform(({ tagId, tagLabel }) => ({ label: tagLabel, id: tagId }));
|
s.type({
|
||||||
export type TagSchema = z.infer<typeof tagSchema>;
|
tagLabel: s.string(),
|
||||||
|
tagId: s.string()
|
||||||
|
}),
|
||||||
|
({ tagId, tagLabel }) => ({ label: tagLabel, id: tagId })
|
||||||
|
);
|
||||||
|
export type TagSchema = s.Infer<typeof tagSchema>;
|
||||||
|
|
||||||
class SuggestionMenu {
|
class SuggestionMenu {
|
||||||
#selectedIndex = 0;
|
#selectedIndex = 0;
|
||||||
|
|
|
@ -1,72 +1,87 @@
|
||||||
import { session } from '@hotwired/turbo';
|
import { session } from '@hotwired/turbo';
|
||||||
import { z } from 'zod';
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
const Gon = z
|
function nullish<T, S>(struct: s.Struct<T, S>) {
|
||||||
.object({
|
return s.optional(s.union([s.literal(null), struct]));
|
||||||
autosave: z
|
}
|
||||||
.object({
|
|
||||||
debounce_delay: z.number().default(0),
|
const Gon = s.defaulted(
|
||||||
status_visible_duration: z.number().default(0)
|
s.type({
|
||||||
})
|
autosave: s.defaulted(
|
||||||
.default({}),
|
s.type({
|
||||||
autocomplete: z
|
debounce_delay: s.defaulted(s.number(), 0),
|
||||||
.object({
|
status_visible_duration: s.defaulted(s.number(), 0)
|
||||||
api_geo_url: z.string().optional(),
|
}),
|
||||||
api_adresse_url: z.string().optional(),
|
{}
|
||||||
api_education_url: z.string().optional()
|
),
|
||||||
})
|
autocomplete: s.defaulted(
|
||||||
.default({}),
|
s.partial(
|
||||||
locale: z.string().default('fr'),
|
s.type({
|
||||||
matomo: z
|
api_geo_url: s.string(),
|
||||||
.object({
|
api_adresse_url: s.string(),
|
||||||
cookieDomain: z.string().optional(),
|
api_education_url: s.string()
|
||||||
domain: z.string().optional(),
|
})
|
||||||
enabled: z.boolean().default(false),
|
),
|
||||||
host: z.string().optional(),
|
{}
|
||||||
key: z.string().or(z.number()).nullish()
|
),
|
||||||
})
|
locale: s.defaulted(s.string(), 'fr'),
|
||||||
.default({}),
|
matomo: s.defaulted(
|
||||||
sentry: z
|
s.type({
|
||||||
.object({
|
cookieDomain: s.optional(s.string()),
|
||||||
key: z.string().nullish(),
|
domain: s.optional(s.string()),
|
||||||
enabled: z.boolean().default(false),
|
enabled: s.defaulted(s.boolean(), false),
|
||||||
environment: z.string().optional(),
|
host: s.optional(s.string()),
|
||||||
user: z.object({ id: z.string() }).default({ id: '' }),
|
key: nullish(s.union([s.string(), s.number()]))
|
||||||
browser: z.object({ modern: z.boolean() }).default({ modern: false }),
|
}),
|
||||||
release: z.string().nullish()
|
{}
|
||||||
})
|
),
|
||||||
.default({}),
|
sentry: s.defaulted(
|
||||||
crisp: z
|
s.type({
|
||||||
.object({
|
key: nullish(s.string()),
|
||||||
key: z.string().nullish(),
|
enabled: s.defaulted(s.boolean(), false),
|
||||||
enabled: z.boolean().default(false),
|
environment: s.optional(s.string()),
|
||||||
administrateur: z
|
user: s.defaulted(s.type({ id: s.string() }), { id: '' }),
|
||||||
.object({
|
browser: s.defaulted(s.type({ modern: s.boolean() }), {
|
||||||
email: z.string(),
|
modern: false
|
||||||
DS_SIGN_IN_COUNT: z.number(),
|
}),
|
||||||
DS_NB_DEMARCHES_BROUILLONS: z.number(),
|
release: nullish(s.string())
|
||||||
DS_NB_DEMARCHES_ACTIVES: z.number(),
|
}),
|
||||||
DS_NB_DEMARCHES_ARCHIVES: z.number(),
|
{}
|
||||||
DS_ID: z.number()
|
),
|
||||||
})
|
crisp: s.defaulted(
|
||||||
.default({
|
s.type({
|
||||||
|
key: nullish(s.string()),
|
||||||
|
enabled: s.defaulted(s.boolean(), false),
|
||||||
|
administrateur: s.defaulted(
|
||||||
|
s.type({
|
||||||
|
email: s.string(),
|
||||||
|
DS_SIGN_IN_COUNT: s.number(),
|
||||||
|
DS_NB_DEMARCHES_BROUILLONS: s.number(),
|
||||||
|
DS_NB_DEMARCHES_ACTIVES: s.number(),
|
||||||
|
DS_NB_DEMARCHES_ARCHIVES: s.number(),
|
||||||
|
DS_ID: s.number()
|
||||||
|
}),
|
||||||
|
{
|
||||||
email: '',
|
email: '',
|
||||||
DS_SIGN_IN_COUNT: 0,
|
DS_SIGN_IN_COUNT: 0,
|
||||||
DS_NB_DEMARCHES_BROUILLONS: 0,
|
DS_NB_DEMARCHES_BROUILLONS: 0,
|
||||||
DS_NB_DEMARCHES_ACTIVES: 0,
|
DS_NB_DEMARCHES_ACTIVES: 0,
|
||||||
DS_NB_DEMARCHES_ARCHIVES: 0,
|
DS_NB_DEMARCHES_ARCHIVES: 0,
|
||||||
DS_ID: 0
|
DS_ID: 0
|
||||||
})
|
}
|
||||||
})
|
)
|
||||||
.default({}),
|
}),
|
||||||
defaultQuery: z.string().optional(),
|
{}
|
||||||
defaultVariables: z.string().optional()
|
),
|
||||||
})
|
defaultQuery: s.optional(s.string()),
|
||||||
.default({});
|
defaultVariables: s.optional(s.string())
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
declare const window: Window & typeof globalThis & { gon: unknown };
|
declare const window: Window & typeof globalThis & { gon: unknown };
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return Gon.parse(window.gon);
|
return s.create(window.gon, Gon);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function show(el: Element | null) {
|
export function show(el: Element | null) {
|
||||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -62,12 +62,12 @@
|
||||||
"react-use-event-hook": "^0.9.6",
|
"react-use-event-hook": "^0.9.6",
|
||||||
"spectaql": "^2.3.1",
|
"spectaql": "^2.3.1",
|
||||||
"stimulus-use": "^0.52.2",
|
"stimulus-use": "^0.52.2",
|
||||||
|
"superstruct": "^1.0.4",
|
||||||
"terser": "^5.31.0",
|
"terser": "^5.31.0",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"trix": "^1.2.3",
|
"trix": "^1.2.3",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0"
|
||||||
"zod": "^3.20.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild/darwin-arm64": "=0.19.9",
|
"@esbuild/darwin-arm64": "=0.19.9",
|
||||||
|
|
Loading…
Reference in a new issue