diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index fcbe0929..da0c4206 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -18,6 +18,7 @@ export const entityConfigSchema = s sort_field: s.string({ default: config.data.default_primary_field }), sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }), primary_format: s.string({ enum: primaryFieldTypes }), + fields_order: s.array(s.string()).optional(), }, { default: {} }, ) @@ -137,7 +138,9 @@ export class Entity< } getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] { - return this.getFields(include_virtual).filter((field) => field.isFillable(context)); + return this.getFields({ virtual: include_virtual }).filter((field) => + field.isFillable(context), + ); } getRequiredFields(): Field[] { @@ -189,9 +192,24 @@ export class Entity< return this.fields.findIndex((field) => field.name === name) !== -1; } - getFields(include_virtual: boolean = false): Field[] { - if (include_virtual) return this.fields; - return this.fields.filter((f) => !f.isVirtual()); + getFields(opts?: { virtual?: boolean; sorted?: boolean }): Field[] { + const fields = [...this.fields]; + const sort = opts?.sorted !== false; + const fields_order = this.config.fields_order; + if (fields_order && fields_order.length > 0 && sort) { + if (fields_order.length !== this.fields.length) { + $console.warn("Fields order must contain all fields"); + } else { + // sort fields by order + fields.sort((a, b) => { + const a_index = fields_order.indexOf(a.name); + const b_index = fields_order.indexOf(b.name); + return a_index - b_index; + }); + } + } + if (opts?.virtual) return fields; + return fields.filter((f) => !f.isVirtual()); } addField(field: Field) { @@ -275,7 +293,7 @@ export class Entity< fields = this.getFillableFields(options.context); break; default: - fields = this.getFields(true); + fields = this.getFields({ virtual: true }); } const _fields = Object.fromEntries(fields.map((field) => [field.name, field])); @@ -309,7 +327,12 @@ export class Entity< toJSON() { return { type: this.type, - fields: Object.fromEntries(this.fields.map((field) => [field.name, field.toJSON()])), + fields: Object.fromEntries( + this.getFields({ virtual: true, sorted: true }).map((field) => [ + field.name, + field.toJSON(), + ]), + ), config: this.config, }; } diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index ab2d15fa..98303736 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -76,7 +76,7 @@ export class SchemaManager { } getIntrospectionFromEntity(entity: Entity): IntrospectedTable { - const fields = entity.getFields(false); + const fields = entity.getFields({ virtual: false, sorted: true }); const indices = this.em.getIndicesOf(entity); // this is intentionally setting values to defaults, like "nullable" and "default" diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index 98ee85ab..e1e54d61 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -120,17 +120,20 @@ function entityFieldActions(bkndActions: TSchemaActions, entityName: string) { return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated); }, patch: () => null, - set: async (fields: TAppDataEntityFields) => { + set: async (fields: TAppDataEntityFields, order?: string[]) => { try { const validated = parse(entityFields, fields, { skipMark: true, forceParse: true, }); - const res = await bkndActions.overwrite( - "data", - `entities.${entityName}.fields`, - validated, - ); + await bkndActions.overwrite("data", `entities.${entityName}.fields`, validated); + if (order) { + await bkndActions.overwrite( + "data", + `entities.${entityName}.config.fields_order`, + order, + ); + } } catch (e) { console.error("error", e); if (e instanceof InvalidSchemaError) { diff --git a/app/src/ui/routes/data/data.schema.$entity.tsx b/app/src/ui/routes/data/data.schema.$entity.tsx index 147a0478..b34d1ddd 100644 --- a/app/src/ui/routes/data/data.schema.$entity.tsx +++ b/app/src/ui/routes/data/data.schema.$entity.tsx @@ -155,18 +155,25 @@ const Fields = ({ entity }: { entity: Entity }) => { const { readonly } = useBknd(); const [res, setRes] = useState(); const ref = useRef(null); + + // @todo: the return of toJSON from Fields doesn't match "type" enum + const initialFields = Object.fromEntries(entity.fields.map((f) => [f.name, f.toJSON()])) as any; + async function handleUpdate() { if (submitting) return; setSubmitting(true); const fields = ref.current?.getData()!; + await actions.entity.patch(entity.name).fields.set(fields); + + // check if field order has changed + if (Object.keys(initialFields).join(",") !== Object.keys(fields).join(",")) { + await actions.entity.patch(entity.name).fields.set(fields, Object.keys(fields)); + } setSubmitting(false); setUpdates((u) => u + 1); } - // @todo: the return of toJSON from Fields doesn't match "type" enum - const initialFields = Object.fromEntries(entity.fields.map((f) => [f.name, f.toJSON()])) as any; - return (