From af573cc79a7b911ec848b499de2dc583def1d9af Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 5 Sep 2025 17:09:50 +0200 Subject: [PATCH] ui: reflect readonly mode by hiding controls + various small styling fixes --- .../ui/components/form/Formy/components.tsx | 2 +- .../form/json-schema-form/Field.tsx | 1 + .../components/form/json-schema-form/Form.tsx | 6 +++- app/src/ui/layouts/AppShell/AppShell.tsx | 12 +++---- .../fields/EntityRelationalFormField.tsx | 2 +- .../schema/create-modal/step.entity.tsx | 4 ++- .../ui/routes/auth/auth.roles.edit.$role.tsx | 5 ++- app/src/ui/routes/auth/auth.roles.tsx | 11 +++++-- app/src/ui/routes/auth/auth.settings.tsx | 25 ++++++++++----- app/src/ui/routes/auth/auth.strategies.tsx | 18 ++++++----- app/src/ui/routes/data/_data.root.tsx | 31 ++++++++++++------- app/src/ui/routes/data/data.$entity.$id.tsx | 20 ++++++++++-- .../ui/routes/data/data.$entity.create.tsx | 21 ++++++++++--- app/src/ui/routes/data/data.$entity.index.tsx | 2 ++ app/src/ui/routes/media/media.settings.tsx | 27 +++++++++++----- .../ui/routes/settings/components/Setting.tsx | 12 +++---- .../settings/components/SettingNewModal.tsx | 22 +++++++------ .../routes/settings/routes/data.settings.tsx | 6 ++-- 18 files changed, 153 insertions(+), 74 deletions(-) diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 64d0f7a..3e95f00 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -88,7 +88,7 @@ export const Input = forwardRef> {...props} ref={ref} className={twMerge( - "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full", + "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full disabled:cursor-not-allowed", disabledOrReadonly && "bg-muted/50 text-primary/50", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index 53820eb..115c79e 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -88,6 +88,7 @@ const FieldImpl = ({ }, [inputProps?.defaultValue]); const disabled = firstDefined( + ctx.readOnly, inputProps?.disabled, props.disabled, schema.readOnly, diff --git a/app/src/ui/components/form/json-schema-form/Form.tsx b/app/src/ui/components/form/json-schema-form/Form.tsx index 5aedfcc..d52280e 100644 --- a/app/src/ui/components/form/json-schema-form/Form.tsx +++ b/app/src/ui/components/form/json-schema-form/Form.tsx @@ -61,6 +61,7 @@ export type FormContext = { options: FormOptions; root: string; _formStateAtom: PrimitiveAtom>; + readOnly: boolean; }; const FormContext = createContext>(undefined!); @@ -81,6 +82,7 @@ export function Form< hiddenSubmit = true, ignoreKeys = [], options = {}, + readOnly = false, ...props }: Omit, "onChange" | "onSubmit"> & { schema: Schema; @@ -93,6 +95,7 @@ export function Form< hiddenSubmit?: boolean; options?: FormOptions; initialValues?: Schema extends JSONSchema ? FromSchema : never; + readOnly?: boolean; }) { const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues); const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]); @@ -190,8 +193,9 @@ export function Form< options, root: "", path: "", + readOnly, }), - [schema, initialValues, options], + [schema, initialValues, options, readOnly], ) as any; return ( diff --git a/app/src/ui/layouts/AppShell/AppShell.tsx b/app/src/ui/layouts/AppShell/AppShell.tsx index ad89ce0..3df68ea 100644 --- a/app/src/ui/layouts/AppShell/AppShell.tsx +++ b/app/src/ui/layouts/AppShell/AppShell.tsx @@ -339,15 +339,15 @@ export const SectionHeaderLink = ({ - {children} + {children} {badge ? ( {badge} @@ -365,8 +365,8 @@ export type SectionHeaderTabsProps = { }; export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => { return ( - -
+ +
{title && ( {title} )} diff --git a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx index 941f899..8ba4f15 100644 --- a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx +++ b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx @@ -232,7 +232,7 @@ const PopoverTable = ({ data={container ?? []} entity={entity} select={query.select} - total={container.meta?.count} + total={container.body.meta?.count} page={query.page} onClickRow={onClickRow} onClickPage={onClickPage} diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx index 4384c3a..0439dca 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.entity.tsx @@ -8,7 +8,9 @@ import { s } from "bknd/utils"; import { cloneSchema } from "core/utils/schema"; const schema = s.object({ - name: entitySchema.properties.name, + name: s.string({ + pattern: /^[a-z][a-zA-Z_]+$/, + }), config: entitySchema.properties.config.partial().optional(), }); type Schema = s.Static; diff --git a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx index 7b70046..f4195e0 100644 --- a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx +++ b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx @@ -26,6 +26,7 @@ function AuthRolesEditInternal({ params }) { const roleName = params.role; const role = config.roles?.[roleName]; const formRef = useRef(null); + const { readonly } = useBknd(); async function handleUpdate() { console.log("data", formRef.current?.isValid()); @@ -57,7 +58,7 @@ function AuthRolesEditInternal({ params }) { absolute: true, }), }, - { + !readonly && { label: "Delete", onClick: handleDelete, destructive: true, @@ -67,9 +68,11 @@ function AuthRolesEditInternal({ params }) { > + !readonly && ( + ) } className="pl-3" diff --git a/app/src/ui/routes/auth/auth.roles.tsx b/app/src/ui/routes/auth/auth.roles.tsx index fa0e521..59c7e22 100644 --- a/app/src/ui/routes/auth/auth.roles.tsx +++ b/app/src/ui/routes/auth/auth.roles.tsx @@ -11,10 +11,12 @@ import { Button } from "../../components/buttons/Button"; import { CellValue, DataTable } from "../../components/table/DataTable"; import * as AppShell from "../../layouts/AppShell/AppShell"; import { routes, useNavigate } from "../../lib/routes"; +import { useBknd } from "ui/client/bknd"; export function AuthRolesList() { const [navigate] = useNavigate(); const { config, actions } = useBkndAuth(); + const { readonly } = useBknd(); const data = Object.values( transformObject(config.roles ?? {}, (role, name) => ({ @@ -30,6 +32,7 @@ export function AuthRolesList() { } function openCreateModal() { + if (readonly) return; bkndModals.open( "form", { @@ -59,9 +62,11 @@ export function AuthRolesList() { <> - Create new - + !readonly && ( + + ) } > Roles & Permissions diff --git a/app/src/ui/routes/auth/auth.settings.tsx b/app/src/ui/routes/auth/auth.settings.tsx index 1ec1403..11e4fb6 100644 --- a/app/src/ui/routes/auth/auth.settings.tsx +++ b/app/src/ui/routes/auth/auth.settings.tsx @@ -52,6 +52,7 @@ const formConfig = { function AuthSettingsInternal() { const { config, schema: _schema, actions, $auth } = useBkndAuth(); + const { readonly } = useBknd(); const schema = JSON.parse(JSON.stringify(_schema)); schema.properties.jwt.required = ["alg"]; @@ -61,7 +62,13 @@ function AuthSettingsInternal() { } return ( -
+ ({ dirty: state.dirty, @@ -73,13 +80,15 @@ function AuthSettingsInternal() { - Update - + !readonly && ( + + ) } > Settings diff --git a/app/src/ui/routes/auth/auth.strategies.tsx b/app/src/ui/routes/auth/auth.strategies.tsx index e7d18d0..a148643 100644 --- a/app/src/ui/routes/auth/auth.strategies.tsx +++ b/app/src/ui/routes/auth/auth.strategies.tsx @@ -60,6 +60,7 @@ const formOptions = { }; function AuthStrategiesListInternal() { + const { readonly } = useBknd(); const $auth = useBkndAuth(); const config = $auth.config.strategies; const schema = $auth.schema.properties.strategies; @@ -80,6 +81,7 @@ function AuthStrategiesListInternal() { initialValues={config} onSubmit={handleSubmit} options={formOptions} + readOnly={readonly} > ({ @@ -92,13 +94,15 @@ function AuthStrategiesListInternal() { - Update - + !readonly && ( + + ) } > Strategies diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index ce195ed..fb4bc2f 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -112,18 +112,27 @@ const EntityLinkList = ({ suggestCreate = false, }: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => { const { $data } = useBkndData(); + const { readonly } = useBknd(); const navigate = useRouteNavigate(); + if (entities.length === 0) { - return suggestCreate ? ( - $data.modals.createEntity(), - }} - /> - ) : null; + if (suggestCreate) { + if (readonly) { + return ; + } + return ( + $data.modals.createEntity(), + }} + /> + ); + } + + return null; } function handleClick(entity: Entity) { @@ -163,7 +172,7 @@ const EntityLinkList = ({ href={href} className="justify-between items-center" > - {entity.label} + {entity.label} {isLinkActive(href, 1) && ( + !readonly && ( + + ) } > Settings @@ -132,6 +141,7 @@ const AdapterIcon = ({ type }: { type: string }) => { function Adapters() { const ctx = AnyOf.useContext(); + const { readonly } = useBknd(); return ( @@ -150,6 +160,7 @@ function Adapters() { "flex flex-row items-center justify-center gap-3 border", ctx.selected === i && "border-primary", )} + disabled={readonly} >
diff --git a/app/src/ui/routes/settings/components/Setting.tsx b/app/src/ui/routes/settings/components/Setting.tsx index b2f81f5..f9565a4 100644 --- a/app/src/ui/routes/settings/components/Setting.tsx +++ b/app/src/ui/routes/settings/components/Setting.tsx @@ -107,8 +107,8 @@ export function Setting({ return; }); - const deleteAllowed = options?.allowDelete?.(config) ?? true; - const editAllowed = options?.allowEdit?.(config) ?? true; + const deleteAllowed = (options?.allowDelete?.(config) ?? true) && !readonly; + const editAllowed = (options?.allowEdit?.(config) ?? true) && !readonly; const showAlert = options?.showAlert?.(config) ?? undefined; console.log("--setting", { schema, config, prefix, path, exclude }); @@ -120,14 +120,14 @@ export function Setting({ extractedKeys.find((key) => window.location.pathname.endsWith(key)) ?? extractedKeys[0]; const onToggleEdit = useEvent(() => { - if (!editAllowed || readonly) return; + if (!editAllowed) return; setEditing((prev) => !prev); //formRef.current?.cancel(); }); const onSave = useEvent(async () => { - if (!editAllowed || !editing || readonly) return; + if (!editAllowed || !editing) return; if (formRef.current?.validateForm()) { setSubmitting(true); @@ -215,14 +215,14 @@ export function Setting({ > - {editing && ( diff --git a/app/src/ui/routes/settings/components/SettingNewModal.tsx b/app/src/ui/routes/settings/components/SettingNewModal.tsx index 07cf922..2481969 100644 --- a/app/src/ui/routes/settings/components/SettingNewModal.tsx +++ b/app/src/ui/routes/settings/components/SettingNewModal.tsx @@ -30,7 +30,7 @@ export const SettingNewModal = ({ const [location, navigate] = useLocation(); const [formSchema, setFormSchema] = useState(schema); const [submitting, setSubmitting] = useState(false); - const { actions } = useBknd(); + const { actions, readonly } = useBknd(); const [opened, { open, close }] = useDisclosure(false); const isGeneratedKey = generateKey !== undefined; const isStaticGeneratedKey = typeof generateKey === "string"; @@ -98,15 +98,17 @@ export const SettingNewModal = ({ return ( <> -
- {isAnyOf ? ( - - - - ) : ( - - )} -
+ {!readonly && ( +
+ {isAnyOf ? ( + + + + ) : ( + + )} +
+ )} { - const { app } = useBknd(); + const { app, readonly } = useBknd(); const prefix = app.getAbsolutePath("settings"); const entities = Object.keys(config.entities ?? {}); @@ -105,7 +105,7 @@ export const DataSettings = ({ options={{ showAlert: (config: any) => { // it's weird, but after creation, the config is not set (?) - if (config?.type === "primary") { + if (config?.type === "primary" && !readonly) { return "Modifying the primary field may result in strange behaviors."; } return; @@ -137,7 +137,7 @@ export const DataSettings = ({ config={config.entities?.[entity] as any} options={{ showAlert: (config: any) => { - if (config.type === "system") { + if (config.type === "system" && !readonly) { return "Modifying the system entities may result in strange behaviors."; } return;