From 083cc9e23e70bdbfcec12ff1290edf000466820b Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Tue, 20 Oct 2026 00:29:01 +0200 Subject: [PATCH] feat: schema builder's create flow --- features/schema_builder_create.feature | 516 +++++++++++++ features/schema_builder_create.qa.md | 82 ++ features/schema_builtin_view.feature | 1 - features/schema_builtin_view.qa.md | 2 +- web/src/assets/main.css | 10 + web/src/components/ApiPreview.vue | 97 +++ web/src/components/ApiPreviewSheet.vue | 48 ++ web/src/components/AuthorList.vue | 3 +- web/src/components/BuilderActions.vue | 42 + web/src/components/BuilderBadge.vue | 46 ++ web/src/components/BuilderInspector.vue | 41 + web/src/components/BuilderStatusChip.vue | 49 ++ web/src/components/BuilderTypeBar.vue | 93 +++ web/src/components/ChoiceList.vue | 175 +++++ web/src/components/DashboardShell.vue | 9 +- web/src/components/DirectoryField.vue | 5 +- web/src/components/DirectoryLoadError.vue | 26 +- web/src/components/FieldActions.vue | 44 ++ web/src/components/FieldInspector.vue | 310 ++++++++ web/src/components/FieldList.vue | 156 ++++ web/src/components/FieldRow.vue | 111 +++ web/src/components/FieldSheet.vue | 63 ++ web/src/components/FieldTypePicker.vue | 66 ++ web/src/components/RetryAlert.vue | 34 + web/src/components/SlugField.vue | 25 +- .../components/__tests__/ApiPreview.spec.ts | 58 ++ .../components/__tests__/ChoiceList.spec.ts | 176 +++++ .../__tests__/FieldInspector.spec.ts | 107 +++ .../components/__tests__/FieldList.spec.ts | 141 ++++ .../__tests__/useContentTypes.spec.ts | 58 ++ .../__tests__/useSchemaGate.spec.ts | 59 ++ web/src/composables/useContentTypes.ts | 11 +- web/src/composables/useJsonCopy.ts | 10 + web/src/composables/useKeyboardReorder.ts | 125 +++ web/src/composables/useSchemaBuilder.ts | 473 ++++++++++++ web/src/composables/useSchemaGate.ts | 14 +- web/src/i18n/messages.ts | 111 +++ web/src/lib/__tests__/arrayMove.spec.ts | 22 + .../lib/__tests__/schemaBuilderKeys.spec.ts | 61 ++ .../lib/__tests__/schemaBuilderModel.spec.ts | 109 +++ .../__tests__/schemaBuilderPayload.spec.ts | 190 +++++ .../lib/__tests__/schemaBuilderSample.spec.ts | 74 ++ .../lib/__tests__/schemaBuilderTouch.spec.ts | 18 + .../__tests__/schemaBuilderValidation.spec.ts | 249 ++++++ web/src/lib/arrayMove.ts | 9 + web/src/lib/articleMeta.ts | 10 - web/src/lib/routePaths.ts | 2 + web/src/lib/schemaBuilderKeys.ts | 35 + web/src/lib/schemaBuilderModel.ts | 127 +++ web/src/lib/schemaBuilderPayload.ts | 66 ++ web/src/lib/schemaBuilderSample.ts | 79 ++ web/src/lib/schemaBuilderTouch.ts | 22 + web/src/lib/schemaBuilderValidation.ts | 227 ++++++ web/src/lib/sheetUi.ts | 10 + web/src/router/__tests__/routerGuard.spec.ts | 38 + web/src/router/index.ts | 7 +- web/src/stores/__tests__/contentTypes.spec.ts | 85 ++ web/src/stores/contentTypes.ts | 36 +- web/src/views/SchemaBuilderView.vue | 287 +++++++ web/src/views/SchemaTypeView.vue | 9 +- .../__tests__/SchemaBuilderErrors.spec.ts | 194 +++++ .../__tests__/SchemaBuilderFields.spec.ts | 727 ++++++++++++++++++ .../__tests__/SchemaBuilderPreview.spec.ts | 156 ++++ .../views/__tests__/SchemaBuilderSave.spec.ts | 487 ++++++++++++ .../__tests__/SchemaBuilderSheet.spec.ts | 65 ++ .../views/__tests__/SchemaBuilderView.spec.ts | 250 ++++++ .../views/__tests__/SchemaTypeView.spec.ts | 4 +- web/src/views/__tests__/builderKit.ts | 107 +++ web/src/views/__tests__/schemaTestKit.ts | 33 +- web/visual/check.ts | 27 + web/visual/states.ts | 65 ++ 71 files changed, 7223 insertions(+), 61 deletions(-) create mode 100644 features/schema_builder_create.feature create mode 100644 features/schema_builder_create.qa.md create mode 100644 web/src/components/ApiPreview.vue create mode 100644 web/src/components/ApiPreviewSheet.vue create mode 100644 web/src/components/BuilderActions.vue create mode 100644 web/src/components/BuilderBadge.vue create mode 100644 web/src/components/BuilderInspector.vue create mode 100644 web/src/components/BuilderStatusChip.vue create mode 100644 web/src/components/BuilderTypeBar.vue create mode 100644 web/src/components/ChoiceList.vue create mode 100644 web/src/components/FieldActions.vue create mode 100644 web/src/components/FieldInspector.vue create mode 100644 web/src/components/FieldList.vue create mode 100644 web/src/components/FieldRow.vue create mode 100644 web/src/components/FieldSheet.vue create mode 100644 web/src/components/FieldTypePicker.vue create mode 100644 web/src/components/RetryAlert.vue create mode 100644 web/src/components/__tests__/ApiPreview.spec.ts create mode 100644 web/src/components/__tests__/ChoiceList.spec.ts create mode 100644 web/src/components/__tests__/FieldInspector.spec.ts create mode 100644 web/src/components/__tests__/FieldList.spec.ts create mode 100644 web/src/composables/__tests__/useSchemaGate.spec.ts create mode 100644 web/src/composables/useJsonCopy.ts create mode 100644 web/src/composables/useKeyboardReorder.ts create mode 100644 web/src/composables/useSchemaBuilder.ts create mode 100644 web/src/lib/__tests__/arrayMove.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderKeys.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderModel.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderPayload.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderSample.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderTouch.spec.ts create mode 100644 web/src/lib/__tests__/schemaBuilderValidation.spec.ts create mode 100644 web/src/lib/arrayMove.ts create mode 100644 web/src/lib/routePaths.ts create mode 100644 web/src/lib/schemaBuilderKeys.ts create mode 100644 web/src/lib/schemaBuilderModel.ts create mode 100644 web/src/lib/schemaBuilderPayload.ts create mode 100644 web/src/lib/schemaBuilderSample.ts create mode 100644 web/src/lib/schemaBuilderTouch.ts create mode 100644 web/src/lib/schemaBuilderValidation.ts create mode 100644 web/src/lib/sheetUi.ts create mode 100644 web/src/views/SchemaBuilderView.vue create mode 100644 web/src/views/__tests__/SchemaBuilderErrors.spec.ts create mode 100644 web/src/views/__tests__/SchemaBuilderFields.spec.ts create mode 100644 web/src/views/__tests__/SchemaBuilderPreview.spec.ts create mode 100644 web/src/views/__tests__/SchemaBuilderSave.spec.ts create mode 100644 web/src/views/__tests__/SchemaBuilderSheet.spec.ts create mode 100644 web/src/views/__tests__/SchemaBuilderView.spec.ts create mode 100644 web/src/views/__tests__/builderKit.ts diff --git a/features/schema_builder_create.feature b/features/schema_builder_create.feature new file mode 100644 index 0000000..a226601 --- /dev/null +++ b/features/schema_builder_create.feature @@ -0,0 +1,516 @@ +Feature: Create a custom content type in the Schema builder + As an owner or admin + I want to define a new content type (name, key and ordered fields) on /schema/new + So that users can write entries of that type + + Background: + Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie + And API errors are plain text + And POST /v1/content-type takes {key, name, fields[{key, label, type, required, unique, default, min, max, options}]} + And the builder never sends an "icon" key + And POST answers 201 with the content type, 400 invalid_content_type_key, 409 content_type_key_taken, 422 invalid_field, 403 or 500 + And the route /schema/new is declared before /schema/:key, has meta auth "verified" and title "Schema", and renders the builder + And a route meta flag, not a path pattern, tells the shell that /schema/new needs the custom list only + And only owners and admins can use Schema; the SPA decides from the session role, the API still enforces it + And the six field types are text "Text", richtext "Rich text", number "Number", boolean "Yes or no", date "Date" and select "Choice" + And the limits are a type name of up to 100 characters, a type key of 2 to 40, 1 to 50 fields, a field key of up to 40 characters and 1 to 50 choices + And the layout is: Name and Key with the status chip, Discard and Save on top; the FieldList on the left; the FieldInspector on the right; the ApiPreview at the bottom + And below 48rem the FieldList is the page, the FieldInspector is a 92% bottom sheet closed with "Done", the ApiPreview is a bottom sheet, and Discard and Save sit in a bottom bar + And every string goes through t() and has a French entry in i18n/messages.ts + And a copy tagged "(proposed copy)" is not in the design and is the coder's single point of change + + Scenario: Reaching the builder + Given the Schema list is Populated or Empty + When the user activates "New content type" (header button, empty-state button or the mobile floating button) + Then the router navigates to /schema/new + And the builder is shown, not the "Content type not found" page + And /schema/events and /schema/unknown still show "Content type not found" for a key that is not built-in + + Scenario: Roles and sessions + Given opens /schema/new + Then + + Examples: + | who | result | + | a verified owner or admin | the builder | + | a verified user who is neither | "You don't have access to Schema" with "Go to Home", URL unchanged | + | an unverified user | the redirect to /register/pending | + | a signed-out visitor | the redirect to /login | + + Scenario: The builder needs no built-in list + Given an owner opens /schema/new as the first page of the session + Then GET /v1/content-type is called once for the sidebar + And GET /v1/builtin-type is never called + And the builder is shown as soon as the session is ready, with no skeleton + + Scenario: New type, desktop (design state 1) + Given an owner opens /schema/new on a 1440 px viewport + Then the breadcrumb is "Schema" (a link to /schema) then "New content type" + And "Name" is an empty text input with the placeholder "For example, Events", marked required, with maxlength 100 + And "Key" is a read-only mono input with the placeholder "events" and the help "Used in URLs and the API. Lowercase letters, digits and hyphens. It can't change after you save." + And the list header reads "Fields" and "1 of 50" + And one row is shown with the label "Title" and the key "title" in mono, selected, with the badge "New" + And "Add field" is shown under the list + And the inspector title is "Field: Title" with the badge "New" + And "Label" holds "Title" + And "Key" holds "title" with the Auto badge and the help "The name used in the API. Starts with a letter. Lowercase letters, digits and underscores." + And the type tiles show Text selected + And "Required" is off with "An entry can't be saved with this field empty." + And "Unique values" is off + And "Default value" is empty with "Used when an entry is created and this field is left out." + And "Shortest" and "Longest" are empty with "Counted in characters. Entries show a counter." + And "Save" and "Discard" are disabled + And no status chip is shown + And no "entries use this content type" bar is shown + And the API preview bar is collapsed + + Scenario: New type, mobile (design state 1) + Given the same page on a 390 px viewport + Then the topbar shows an arrow-left and the title "New content type" + And Name and Key are stacked above the list, then the rows, then the "API preview" row with a chevron + And no inspector is shown until a row is activated + And the bottom bar holds Discard and Save + + Scenario Outline: The key follows the name until it is edited + Given an empty new type + When the user types the name "" + Then the Key holds "" with the Auto badge, read-only + + Examples: + | name | key | + | Events | events | + | Press mentions | press-mentions | + | Café & Bar | cafe-bar | + | Job openings! | job-openings | + | 2026 Awards | 2026-awards | + | !!! | (empty, mode empty) | + | a name of 60 letters | the first 40 characters, without a trailing hyphen | + + Scenario: Editing and resetting the key + Given the name "Events" and the Auto key "events" + When the user activates the pencil + Then the Key becomes editable, the Auto badge goes and "Reset to match the name" is shown + When the user types the key "agenda" then edits the name to "Events and talks" + Then the Key stays "agenda" + When the user activates "Reset to match the name" + Then the Key is "events-and-talks" with the Auto badge + + Scenario Outline: Type-level rules (design states 6 and 14) + Given a new type with a valid field + When the user sets to and activates Save + Then is shown under with an alert icon + And POST /v1/content-type is not called + + Examples: + | input | value | message | + | Name | (empty or spaces) | Give the content type a name. | + | Key | e | Use 2 to 40 lowercase letters, digits or hyphens. | + | Key | Events | Use 2 to 40 lowercase letters, digits or hyphens. | + | Key | my_type | Use 2 to 40 lowercase letters, digits or hyphens. | + | Key | -events | Use 2 to 40 lowercase letters, digits or hyphens. | + | Key | events--2 | Use 2 to 40 lowercase letters, digits or hyphens. | + | Key | new | The key "new" is reserved. Choose a different key. (proposed copy) | + + Scenario Outline: Accepted type keys + Given a new type with a valid field and the name "X" + When the user types the key "" and activates Save + Then POST /v1/content-type is called with that key + + Examples: + | key | + | ab | + | press-mentions | + | 2026-awards | + | 40 lowercase letters | + + Scenario: Type-level errors together (design state 6) + Given a new type with no name, the key "e" and no field + When the user activates Save + Then "Give the content type a name.", "Use 2 to 40 lowercase letters, digits or hyphens." and "A content type needs at least one field." are shown + And the empty list shows "No fields yet" with the message in the error colour + And the chip reads "3 problems to fix" + And Save is disabled until all three are fixed, and Discard stays enabled + And the inspector shows "Field settings" and "Add a field, then choose its type and rules here." + + Scenario: When errors are shown + Given a new type whose name is empty + Then no error is shown while nothing was edited + When the user edits an input, an error for that input shows as they type + And when the user activates Save, every error of the form shows at once + And errors are tied to their input with aria-describedby and aria-invalid + And a row with an error shows an alert icon, not only the red border + + Scenario Outline: The status chip + Given + Then the chip reads "" + + Examples: + | situation | chip | + | a pristine new type | (no chip) | + | an edit with no error | Unsaved changes | + | three inputs in error | 3 problems to fix | + | one input in error | 1 problem to fix | + | the POST in flight | Saving… | + | the POST failed with 500 | Save failed | + | navigator.onLine is false | Waiting for connection | + + Scenario Outline: Counting problems + Given + Then the chip counts + + Examples: + | errors | count | + | no name, a one-letter key, no field | 3 | + | a field with no label, a field with min above max, a choice list with a blank and a duplicate | 3 | + | one choice list with a blank row and a duplicate row | 1 | + | one field with a bad key | 1 | + + Scenario: Field list rows + Given a type with the fields Title, Starts on, Online + Then each row shows a grip button labelled "Move