@abgov/nx-adsp 13.17.1 → 13.18.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +134 -12
  2. package/generators.json +20 -0
  3. package/package.json +1 -1
  4. package/src/generators/vue-admin-crud/files/src/views/__editViewFileName__.vue__tmpl__ +136 -0
  5. package/src/generators/vue-admin-crud/files/src/views/__listViewFileName__.vue__tmpl__ +76 -0
  6. package/src/generators/vue-admin-crud/schema.d.ts +34 -0
  7. package/src/generators/vue-admin-crud/schema.json +54 -0
  8. package/src/generators/vue-admin-crud/vue-admin-crud.d.ts +3 -0
  9. package/src/generators/vue-admin-crud/vue-admin-crud.js +55 -0
  10. package/src/generators/vue-admin-crud/vue-admin-crud.js.map +1 -0
  11. package/src/generators/vue-admin-crud/vue-admin-crud.spec.ts +186 -0
  12. package/src/generators/vue-app/files/AGENTS.md__tmpl__ +71 -7
  13. package/src/generators/vue-app/files/src/App.spec.ts__tmpl__ +69 -2
  14. package/src/generators/vue-app/files/src/App.vue__tmpl__ +80 -15
  15. package/src/generators/vue-app/files/src/main.ts__tmpl__ +10 -1
  16. package/src/generators/vue-app/files/src/stores/session.ts__tmpl__ +16 -0
  17. package/src/generators/vue-app/files/src/views/HomeView.vue__tmpl__ +4 -0
  18. package/src/generators/vue-app/schema.d.ts +2 -0
  19. package/src/generators/vue-app/schema.json +6 -0
  20. package/src/generators/vue-app/vue-app.js +5 -1
  21. package/src/generators/vue-app/vue-app.js.map +1 -1
  22. package/src/generators/vue-app/vue-app.spec.ts +131 -14
  23. package/src/generators/vue-components/files/AGENTS.md__tmpl__ +63 -15
  24. package/src/generators/vue-components/files/src/index.ts__tmpl__ +28 -12
  25. package/src/generators/vue-components/files/src/lib/patterns/AppFooter.vue__tmpl__ +14 -0
  26. package/src/generators/vue-components/files/src/lib/patterns/AppHeader.vue__tmpl__ +19 -0
  27. package/src/generators/{vue-app/files/src/components → vue-components/files/src/lib/patterns}/AppLayout.vue__tmpl__ +9 -5
  28. package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +119 -0
  29. package/src/generators/vue-components/files/src/lib/patterns/RecordDetailShell.vue__tmpl__ +75 -0
  30. package/src/generators/vue-components/files/src/lib/patterns/SessionExpiredBanner.vue__tmpl__ +28 -0
  31. package/src/generators/vue-components/files/src/lib/patterns/StepErrorSummary.vue__tmpl__ +26 -0
  32. package/src/generators/vue-components/files/src/lib/patterns/Stepper.vue__tmpl__ +35 -0
  33. package/src/generators/vue-components/files/src/lib/patterns/WorkspaceTable.vue__tmpl__ +142 -0
  34. package/src/generators/vue-components/files/src/vue-components.spec.ts__tmpl__ +20 -4
  35. package/src/generators/vue-components/vue-components.spec.ts +32 -3
  36. package/src/generators/vue-detail-view/files/src/views/__viewFileName__.vue__tmpl__ +91 -0
  37. package/src/generators/vue-detail-view/schema.d.ts +29 -0
  38. package/src/generators/vue-detail-view/schema.json +50 -0
  39. package/src/generators/vue-detail-view/vue-detail-view.d.ts +3 -0
  40. package/src/generators/vue-detail-view/vue-detail-view.js +50 -0
  41. package/src/generators/vue-detail-view/vue-detail-view.js.map +1 -0
  42. package/src/generators/vue-detail-view/vue-detail-view.spec.ts +156 -0
  43. package/src/generators/vue-intake-view/files/shared/src/views/__confirmationViewFileName__.vue__tmpl__ +20 -0
  44. package/src/generators/vue-intake-view/files/shared/src/views/__reviewViewFileName__.vue__tmpl__ +121 -0
  45. package/src/generators/vue-intake-view/files/steps/src/views/__stepViewFileName__.vue__tmpl__ +162 -0
  46. package/src/generators/vue-intake-view/schema.d.ts +33 -0
  47. package/src/generators/vue-intake-view/schema.json +46 -0
  48. package/src/generators/vue-intake-view/vue-intake-view.d.ts +3 -0
  49. package/src/generators/vue-intake-view/vue-intake-view.js +85 -0
  50. package/src/generators/vue-intake-view/vue-intake-view.js.map +1 -0
  51. package/src/generators/vue-intake-view/vue-intake-view.spec.ts +194 -0
  52. package/src/generators/vue-workspace-view/files/src/views/__viewFileName__.vue__tmpl__ +158 -0
  53. package/src/generators/vue-workspace-view/schema.d.ts +37 -0
  54. package/src/generators/vue-workspace-view/schema.json +64 -0
  55. package/src/generators/vue-workspace-view/vue-workspace-view.d.ts +3 -0
  56. package/src/generators/vue-workspace-view/vue-workspace-view.js +51 -0
  57. package/src/generators/vue-workspace-view/vue-workspace-view.js.map +1 -0
  58. package/src/generators/vue-workspace-view/vue-workspace-view.spec.ts +183 -0
  59. package/src/utils/vue-router.d.ts +8 -0
  60. package/src/utils/vue-router.js +32 -0
  61. package/src/utils/vue-router.js.map +1 -0
  62. package/src/utils/vue-router.spec.ts +71 -0
  63. /package/src/generators/vue-components/files/src/lib/{GoabButton.vue__tmpl__ → primitives/GoabButton.vue__tmpl__} +0 -0
  64. /package/src/generators/vue-components/files/src/lib/{GoabCheckbox.vue__tmpl__ → primitives/GoabCheckbox.vue__tmpl__} +0 -0
  65. /package/src/generators/vue-components/files/src/lib/{GoabDropdown.vue__tmpl__ → primitives/GoabDropdown.vue__tmpl__} +0 -0
  66. /package/src/generators/vue-components/files/src/lib/{GoabInput.vue__tmpl__ → primitives/GoabInput.vue__tmpl__} +0 -0
  67. /package/src/generators/vue-components/files/src/lib/{GoabModal.vue__tmpl__ → primitives/GoabModal.vue__tmpl__} +0 -0
  68. /package/src/generators/vue-components/files/src/lib/{GoabRadioGroup.vue__tmpl__ → primitives/GoabRadioGroup.vue__tmpl__} +0 -0
  69. /package/src/generators/vue-components/files/src/lib/{GoabTextarea.vue__tmpl__ → primitives/GoabTextarea.vue__tmpl__} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Nx plugin for bootstrapping [ADSP](https://govalta.github.io/adsp-monorepo/) applications for the Government of Alberta.
4
4
 
5
- The plugin provides generators for Node/Express services, React and Angular frontends, .NET services, and fullstack solutions. When `@abgov/nx-oc` is also installed in the workspace, OpenShift deployment YAML is automatically included in the generated output.
5
+ The plugin provides generators for Node/Express services, React, Angular, and Vue 3 frontends, .NET services, and fullstack solutions. When `@abgov/nx-oc` is also installed in the workspace, OpenShift deployment YAML is automatically included in the generated output.
6
6
 
7
7
  ## Prerequisites
8
8
 
@@ -10,17 +10,18 @@ Install only the framework peers for the generators you use — they are declare
10
10
  as **optional** peer dependencies, so a workspace that only builds Vue apps
11
11
  doesn't need `@nx/react`, `@nx/angular`, etc.
12
12
 
13
- | Generator | Required peer dependency |
14
- | ------------------------------- | ---------------------------------------- |
15
- | `react-app` | `@nx/react` |
16
- | `angular-app` | `@nx/angular` |
17
- | `vue-app` | `@nx/vue` |
18
- | `dotnet-service` | `@nx-dotnet/core` |
19
- | `react-dotnet` | `@nx/react`, `@nx-dotnet/core` |
20
- | `express-service` | `@nx/express` |
21
- | `pevn`, `mevn` (full-stack) | `@nx/express`, `@nx/vue` |
22
- | `pern`, `pean` (full-stack) | `@nx/express`, `@nx/react`/`@nx/angular` |
23
- | `react-form`, `react-task-list` | existing React project in the workspace |
13
+ | Generator | Required peer dependency |
14
+ | ---------------------------------------------------------------------------- | ------------------------------------------- |
15
+ | `react-app` | `@nx/react` |
16
+ | `angular-app` | `@nx/angular` |
17
+ | `vue-app` | `@nx/vue` |
18
+ | `dotnet-service` | `@nx-dotnet/core` |
19
+ | `react-dotnet` | `@nx/react`, `@nx-dotnet/core` |
20
+ | `express-service` | `@nx/express` |
21
+ | `pevn`, `mevn` (full-stack) | `@nx/express`, `@nx/vue` |
22
+ | `pern`, `pean` (full-stack) | `@nx/express`, `@nx/react`/`@nx/angular` |
23
+ | `react-form`, `react-task-list` | existing React project in the workspace |
24
+ | `vue-detail-view`, `vue-workspace-view`, `vue-admin-crud`, `vue-intake-view` | existing `vue-app` project in the workspace |
24
25
 
25
26
  ## Installation
26
27
 
@@ -185,6 +186,30 @@ Accepts the same options as `react-app`.
185
186
 
186
187
  ---
187
188
 
189
+ ### `vue-app`
190
+
191
+ Creates a Vue 3 frontend application configured for ADSP, using GoA web components
192
+ (`@abgov/web-components`) and `@dsb-norge/vue-keycloak-js` for authentication. Requires
193
+ `@nx/vue`.
194
+
195
+ ```bash
196
+ npx nx g @abgov/nx-adsp:vue-app my-app --env dev --tenant my-tenant
197
+ ```
198
+
199
+ Accepts the same options as `react-app`, plus:
200
+
201
+ | Option | Required | Description |
202
+ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
203
+ | `layout` | No | Top-level app shell: `header` (default) is a `goa-app-header` + hero banner + footer (public-facing); `internal` is a `goa-work-side-menu` shell with no header/banner/footer (staff-facing). Pair two runs against the same `--pairedProject` for a public+internal frontend pairing over one backend |
204
+
205
+ Every generated app provisions a shared `vue-components` library — `Goab*` `v-model` wrappers
206
+ over the design system plus reusable app-shell pattern components (`AppLayout`, `AppHeader`,
207
+ `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`) — see the generated app's own `AGENTS.md`
208
+ for the full contract. Four more generators retrofit common view shapes into an existing
209
+ `vue-app` project; see below.
210
+
211
+ ---
212
+
188
213
  ### `dotnet-service`
189
214
 
190
215
  Creates an ASP.NET Core backend service configured for ADSP. Requires `@nx-dotnet/core`.
@@ -241,6 +266,103 @@ Accepts the same options as `react-form`.
241
266
 
242
267
  ---
243
268
 
269
+ ### `vue-detail-view`
270
+
271
+ Adds a record-detail view (loading/error/loaded states, optional status badge, back button) to
272
+ an existing `vue-app` project, built on the shared `RecordDetailShell` pattern component.
273
+
274
+ ```bash
275
+ npx nx g @abgov/nx-adsp:vue-detail-view my-app --name=application-detail --resource=applications --route=/applications/:id --fields='[{"key":"status","label":"Status","type":"badge"}]'
276
+ ```
277
+
278
+ | Option | Required | Description |
279
+ | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
280
+ | `project` | Yes | The `vue-app` project to add the view to |
281
+ | `name` | Yes | View name, e.g. `application-detail` generates `src/views/ApplicationDetailView.vue` |
282
+ | `resource` | Yes | API resource path segment — the view fetches `/api/<resource>/:id` |
283
+ | `route` | Yes | Route path added to `router/index.ts`, e.g. `/applications/:id`. Must contain a `:id` param |
284
+ | `fields` | Yes | JSON array of fields rendered in the record's info list, in display order: `{ key, label, type?: "text"\|"date"\|"currency"\|"badge" }` — a JSON string, not a comma-separated CLI array (see note below) |
285
+ | `heading` | No | Page heading. Defaults to the view name, title-cased |
286
+ | `requiresAuth` | No | Whether the generated route requires authentication. Defaults to `true` |
287
+
288
+ ---
289
+
290
+ ### `vue-workspace-view`
291
+
292
+ Adds a staff-facing, paginated list view (a debounced search filter bar + sortable columns) to
293
+ an existing `vue-app` project, built on the shared `WorkspaceTable` pattern component.
294
+
295
+ ```bash
296
+ npx nx g @abgov/nx-adsp:vue-workspace-view my-app --name=applications --resource=applications --route=/applications --detailRoute=/applications --columns='[{"key":"status","label":"Status","type":"badge","sortable":true}]'
297
+ ```
298
+
299
+ | Option | Required | Description |
300
+ | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
301
+ | `project` | Yes | The `vue-app` project to add the view to |
302
+ | `name` | Yes | View name, e.g. `applications` generates `src/views/ApplicationsListView.vue` |
303
+ | `resource` | Yes | API resource path segment — fetches `/api/<resource>?page=&limit=&search=&sortBy=&sortDir=` |
304
+ | `route` | Yes | Route path added to `router/index.ts`, e.g. `/applications` |
305
+ | `columns` | Yes | JSON array of table columns, in display order: `{ key, label, type?: "text"\|"date"\|"currency"\|"badge", sortable? }` — a JSON string (see note below) |
306
+ | `detailRoute` | No | If set, each row gets a "View" action linking to `` `${detailRoute}/${row.id}` `` — typically a `vue-detail-view`'s route with the `:id` segment dropped |
307
+ | `filterable` | No | Whether to generate a debounced search input above the table. Defaults to `true` |
308
+ | `heading` | No | Page heading. Defaults to the view name, title-cased |
309
+ | `pageSize` | No | Rows per page. Defaults to `20` |
310
+ | `requiresAuth` | No | Whether the generated route requires authentication. Defaults to `true` |
311
+
312
+ ---
313
+
314
+ ### `vue-admin-crud`
315
+
316
+ Adds a simple admin CRUD screen pair (a `WorkspaceTable` list view with a Create action and
317
+ per-row Edit, plus a create/update Edit view) to an existing `vue-app` project — suited to small
318
+ lookup-table style admin screens, not large paginated workspaces (see `vue-workspace-view` for
319
+ that).
320
+
321
+ ```bash
322
+ npx nx g @abgov/nx-adsp:vue-admin-crud my-app --name=regions --resource=regions --route=/regions --fields='[{"key":"name","label":"Name"},{"key":"active","label":"Active","type":"checkbox"}]'
323
+ ```
324
+
325
+ | Option | Required | Description |
326
+ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
327
+ | `project` | Yes | The `vue-app` project to add the views to |
328
+ | `name` | Yes | View name, e.g. `regions` generates `src/views/RegionsListView.vue` and `src/views/RegionsEditView.vue` |
329
+ | `resource` | Yes | API resource path segment — fetches `/api/<resource>` (list), `/api/<resource>/:id` (load one), `POST /api/<resource>` (create), `PUT /api/<resource>/:id` (update) |
330
+ | `route` | Yes | List route path added to `router/index.ts`, e.g. `/regions`. The edit/create route is added as `` `${route}/:id` `` (visiting `${route}/new` creates) |
331
+ | `fields` | Yes | JSON array of fields, in display/form order: `{ key, label, type?: "text"\|"checkbox", required? }` — a JSON string (see note below) |
332
+ | `heading` | No | List page heading. Defaults to the view name, title-cased |
333
+ | `singularLabel` | No | Singular label used in "Create <label>"/"Edit <label>" headings and buttons. Defaults to `--heading` (override for irregular plurals) |
334
+ | `requiresAuth` | No | Whether the generated routes require authentication. Defaults to `true` |
335
+
336
+ ---
337
+
338
+ ### `vue-intake-view`
339
+
340
+ Adds a route-per-step intake wizard (`Stepper` + `StepErrorSummary`, a required read-only
341
+ review step, and a confirmation page) to an existing `vue-app` project. Cross-step state is
342
+ server-persisted — each step PUTs/POSTs to `/api/<resource>/:id` and refetches on mount, so
343
+ there's no client-side draft caching. Every field is currently a plain text input.
344
+
345
+ ```bash
346
+ npx nx g @abgov/nx-adsp:vue-intake-view my-app --name=application --resource=applications --route=/applications --steps='[{"key":"personal-info","label":"Personal information","fields":[{"key":"fullName","label":"Full name"}]}]'
347
+ ```
348
+
349
+ | Option | Required | Description |
350
+ | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
351
+ | `project` | Yes | The `vue-app` project to add the views to |
352
+ | `name` | Yes | Base name for the generated views, e.g. `application` generates `<Step>StepView.vue` per step plus `ApplicationReviewView.vue`/`ApplicationConfirmationView.vue` |
353
+ | `resource` | Yes | API resource path segment. Each step fetches/saves `/api/<resource>/:id`; the review step's Submit posts `/api/<resource>/:id/submit` |
354
+ | `route` | Yes | Base route, e.g. `/applications`. Steps become `/applications/:id/<step-key>`, plus `/review` and `/confirmation`. Start a new intake at `/applications/new/<first-step-key>` |
355
+ | `steps` | Yes | JSON array of steps, in order: `{ key, label, fields: [{ key, label, required? }] }` — a JSON string (see note below) |
356
+ | `requiresAuth` | No | Whether the generated routes require authentication. Defaults to `true` |
357
+
358
+ ---
359
+
360
+ > **Note on JSON-string options (`fields`/`columns`/`steps`):** Nx's own CLI arg coercion only
361
+ > supports comma-separated primitive lists for `array`-typed schema options, not JSON — a JSON
362
+ > string is the only CLI syntax that survives it, so these options are declared as `string` and
363
+ > parsed internally. A plain array also works when the generator is invoked programmatically
364
+ > (e.g. from a script), bypassing CLI parsing entirely.
365
+
244
366
  ## Authentication
245
367
 
246
368
  Most generators call ADSP APIs during generation, which needs an ADSP access
package/generators.json CHANGED
@@ -60,6 +60,26 @@
60
60
  "description": "Ensures the shared GoA Vue wrapper library (interim). Invoked by vue-app; runnable directly to repair.",
61
61
  "hidden": true
62
62
  },
63
+ "vue-detail-view": {
64
+ "factory": "./src/generators/vue-detail-view/vue-detail-view",
65
+ "schema": "./src/generators/vue-detail-view/schema.json",
66
+ "description": "Generator that adds a record-detail view (RecordDetailShell + a --fields spec) to an existing vue-app project."
67
+ },
68
+ "vue-workspace-view": {
69
+ "factory": "./src/generators/vue-workspace-view/vue-workspace-view",
70
+ "schema": "./src/generators/vue-workspace-view/schema.json",
71
+ "description": "Generator that adds a staff-facing, paginated list view (WorkspaceTable + a --columns spec) to an existing vue-app project."
72
+ },
73
+ "vue-admin-crud": {
74
+ "factory": "./src/generators/vue-admin-crud/vue-admin-crud",
75
+ "schema": "./src/generators/vue-admin-crud/schema.json",
76
+ "description": "Generator that adds a simple admin CRUD screen pair (WorkspaceTable list + create/update Edit view) to an existing vue-app project."
77
+ },
78
+ "vue-intake-view": {
79
+ "factory": "./src/generators/vue-intake-view/vue-intake-view",
80
+ "schema": "./src/generators/vue-intake-view/schema.json",
81
+ "description": "Generator that adds a route-per-step intake wizard (Stepper + a required review/confirmation flow, from a --steps spec) to an existing vue-app project."
82
+ },
63
83
  "mean": {
64
84
  "factory": "./src/generators/mean/mean",
65
85
  "schema": "./src/generators/mean/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "13.17.1",
3
+ "version": "13.18.0-beta.10",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import { reactive, ref, computed, onMounted } from 'vue';
3
+ import { useRoute, useRouter } from 'vue-router';
4
+ import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
5
+ import { GoabInput, GoabCheckbox } from '<%= goaImportPath %>';
6
+
7
+ const route = useRoute();
8
+ const router = useRouter();
9
+ const kc = useKeycloak();
10
+
11
+ const idParam = computed(() => String(route.params.id ?? ''));
12
+ const isNew = computed(() => idParam.value === 'new' || idParam.value === '');
13
+
14
+ const form = reactive({
15
+ <% fields.forEach(function (field) { -%>
16
+ <%- field.key %>: <%- field.type === 'checkbox' ? 'false' : "''" %>,
17
+ <% }); -%>
18
+ });
19
+
20
+ const errors = reactive<Record<string, string | undefined>>({});
21
+ const loading = ref(!isNew.value);
22
+ const saving = ref(false);
23
+ // Separate load vs. save errors -- a save failure must keep the form on
24
+ // screen (with an inline "Save failed" callout), not replace it with the
25
+ // top-level "Unable to load" state a load failure shows instead of the form.
26
+ const loadError = ref<string | null>(null);
27
+ const saveError = ref<string | null>(null);
28
+ const successMessage = ref<string | null>(null);
29
+
30
+ async function load() {
31
+ if (isNew.value) return;
32
+ loading.value = true;
33
+ loadError.value = null;
34
+ try {
35
+ const res = await fetch(`/api/<%= resource %>/${idParam.value}`);
36
+ if (!res.ok) throw new Error(`Failed to load (${res.status})`);
37
+ const data = await res.json();
38
+ <% fields.forEach(function (field) { -%>
39
+ if (data['<%= field.key %>'] !== undefined) form.<%= field.key %> = data['<%= field.key %>'];
40
+ <% }); -%>
41
+ } catch (e) {
42
+ loadError.value = e instanceof Error ? e.message : 'Failed to load.';
43
+ } finally {
44
+ loading.value = false;
45
+ }
46
+ }
47
+
48
+ onMounted(load);
49
+
50
+ function validate(): boolean {
51
+ let valid = true;
52
+ <% fields.forEach(function (field) { -%>
53
+ <% if (field.type !== 'checkbox' && field.required !== false) { -%>
54
+ if (!form.<%= field.key %> || !form.<%= field.key %>.trim()) {
55
+ errors.<%= field.key %> = '<%= field.label %> is required.';
56
+ valid = false;
57
+ } else {
58
+ errors.<%= field.key %> = undefined;
59
+ }
60
+ <% } -%>
61
+ <% }); -%>
62
+ return valid;
63
+ }
64
+
65
+ async function onSubmit() {
66
+ if (!validate()) return;
67
+ saving.value = true;
68
+ saveError.value = null;
69
+ try {
70
+ const res = await fetch(
71
+ isNew.value ? '/api/<%= resource %>' : `/api/<%= resource %>/${idParam.value}`,
72
+ {
73
+ method: isNew.value ? 'POST' : 'PUT',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify(form),
76
+ },
77
+ );
78
+ if (!res.ok) throw new Error(`Failed to save (${res.status})`);
79
+ successMessage.value = isNew.value ? '<%= singularLabel %> created.' : '<%= singularLabel %> saved.';
80
+ setTimeout(() => router.push('<%= route %>'), 600);
81
+ } catch (e) {
82
+ saveError.value = e instanceof Error ? e.message : 'Failed to save.';
83
+ } finally {
84
+ saving.value = false;
85
+ }
86
+ }
87
+
88
+ function goBack() {
89
+ router.push('<%= route %>');
90
+ }
91
+ </script>
92
+
93
+ <template>
94
+ <div>
95
+ <h1>{{ isNew ? 'Create <%= singularLabel %>' : 'Edit <%= singularLabel %>' }}</h1>
96
+
97
+ <goa-callout v-if="successMessage" type="success" heading="Saved">
98
+ <p>{{ successMessage }}</p>
99
+ </goa-callout>
100
+
101
+ <div v-else-if="loading" aria-label="Loading">
102
+ <goa-skeleton type="text" size="3" />
103
+ </div>
104
+
105
+ <goa-callout v-else-if="loadError" type="emergency" heading="Unable to load">
106
+ <p>{{ loadError }}</p>
107
+ </goa-callout>
108
+
109
+ <form v-else @submit.prevent="onSubmit">
110
+ <% fields.forEach(function (field) { -%>
111
+ <% if (field.type === 'checkbox') { -%>
112
+ <goa-form-item label="<%= field.label %>">
113
+ <GoabCheckbox v-model="form.<%= field.key %>" name="<%= field.key %>" text="<%= field.label %>" />
114
+ </goa-form-item>
115
+ <% } else { -%>
116
+ <goa-form-item label="<%= field.label %>"<% if (field.required !== false) { %> requirement="required"<% } %> :error="errors.<%= field.key %>">
117
+ <GoabInput v-model="form.<%= field.key %>" name="<%= field.key %>" type="text" />
118
+ </goa-form-item>
119
+ <% } -%>
120
+ <% }); -%>
121
+
122
+ <goa-callout v-if="saveError" type="emergency" heading="Save failed">
123
+ <p>{{ saveError }}</p>
124
+ </goa-callout>
125
+
126
+ <goa-spacer vspacing="l" />
127
+
128
+ <goa-button-group gap="relaxed">
129
+ <goa-button v-if="kc.authenticated" type="primary" :disabled="saving || undefined" @_click="onSubmit">
130
+ {{ isNew ? 'Create <%= singularLabel %>' : 'Save changes' }}
131
+ </goa-button>
132
+ <goa-button type="secondary" @_click="goBack">Cancel</goa-button>
133
+ </goa-button-group>
134
+ </form>
135
+ </div>
136
+ </template>
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue';
3
+ import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
4
+ import { WorkspaceTable } from '<%= goaImportPath %>';
5
+
6
+ const kc = useKeycloak();
7
+
8
+ const columns = [
9
+ <% fields.forEach(function (field) { -%>
10
+ { key: '<%= field.key %>', label: '<%= field.label %>' },
11
+ <% }); -%>
12
+ ];
13
+
14
+ // The fetched rows' shape isn't known to this generator -- read fields
15
+ // defensively rather than declaring (and likely getting wrong) a fake interface.
16
+ const rows = ref<Record<string, unknown>[]>([]);
17
+ const loading = ref(true);
18
+ const error = ref<string | null>(null);
19
+
20
+ async function load() {
21
+ loading.value = true;
22
+ error.value = null;
23
+ try {
24
+ const res = await fetch('/api/<%= resource %>');
25
+ if (!res.ok) throw new Error(`Failed to load (${res.status})`);
26
+ const data = await res.json();
27
+ // Accept either a bare array or a { results } envelope.
28
+ rows.value = Array.isArray(data) ? data : (data.results ?? []);
29
+ } catch (e) {
30
+ error.value = e instanceof Error ? e.message : 'Failed to load.';
31
+ } finally {
32
+ loading.value = false;
33
+ }
34
+ }
35
+
36
+ onMounted(load);
37
+ </script>
38
+
39
+ <template>
40
+ <div>
41
+ <div class="admin-crud-topbar">
42
+ <h1><%= heading %></h1>
43
+ <router-link v-if="kc.authenticated" to="<%= route %>/new">
44
+ <goa-button type="primary">Create <%= singularLabel %></goa-button>
45
+ </router-link>
46
+ </div>
47
+
48
+ <goa-spacer vspacing="m" />
49
+
50
+ <!-- No page/item-count/per-page-count props -- an admin lookup table this
51
+ small doesn't need WorkspaceTable's pagination. -->
52
+ <WorkspaceTable :columns="columns" :rows="rows" :loading="loading" :error="error" @retry="load">
53
+ <% fields.forEach(function (field) { -%>
54
+ <% if (field.type === 'checkbox') { -%>
55
+ <template #cell-<%= field.key %>="{ row }">
56
+ <goa-badge :type="row['<%= field.key %>'] ? 'success' : 'midtone'" :content="row['<%= field.key %>'] ? 'Yes' : 'No'" />
57
+ </template>
58
+ <% } -%>
59
+ <% }); -%>
60
+ <template #actions="{ row }">
61
+ <router-link :to="`<%= route %>/${row.id}`">
62
+ <goa-button type="tertiary" size="compact">Edit</goa-button>
63
+ </router-link>
64
+ </template>
65
+ </WorkspaceTable>
66
+ </div>
67
+ </template>
68
+
69
+ <style scoped>
70
+ .admin-crud-topbar {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: center;
74
+ gap: var(--goa-space-m);
75
+ }
76
+ </style>
@@ -0,0 +1,34 @@
1
+ export interface AdminCrudField {
2
+ key: string;
3
+ label: string;
4
+ type?: 'text' | 'checkbox';
5
+ required?: boolean;
6
+ }
7
+
8
+ export interface Schema {
9
+ project: string;
10
+ name: string;
11
+ resource: string;
12
+ route: string;
13
+ /**
14
+ * JSON string on the real CLI (Nx's array-typed CLI coercion only supports
15
+ * comma-separated primitives, not JSON). A real array is also accepted for
16
+ * programmatic callers (e.g. tests).
17
+ */
18
+ fields: string | AdminCrudField[];
19
+ heading?: string;
20
+ singularLabel?: string;
21
+ requiresAuth?: boolean;
22
+ }
23
+
24
+ export interface NormalizedSchema extends Omit<Schema, 'fields'> {
25
+ projectRoot: string;
26
+ /** PascalCase view name with a "ListView" suffix, e.g. RegionsListView. */
27
+ listViewFileName: string;
28
+ /** PascalCase view name with an "EditView" suffix, e.g. RegionsEditView. */
29
+ editViewFileName: string;
30
+ fields: AdminCrudField[];
31
+ heading: string;
32
+ singularLabel: string;
33
+ requiresAuth: boolean;
34
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "id": "NxAdspVueAdminCrud",
4
+ "title": "Vue Admin CRUD (List + Edit)",
5
+ "description": "Generates a simple admin CRUD screen pair (a WorkspaceTable list view with a Create action and per-row Edit, plus a create/update Edit view) into an existing vue-app project. Suited to small lookup-table style admin screens, not large paginated workspaces -- see vue-workspace-view for that.",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The vue-app project to add the views to.",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ },
15
+ "x-prompt": "Which project should the admin CRUD screens be added to?"
16
+ },
17
+ "name": {
18
+ "type": "string",
19
+ "description": "View name, e.g. 'regions' generates src/views/RegionsListView.vue and src/views/RegionsEditView.vue.",
20
+ "$default": {
21
+ "$source": "argv",
22
+ "index": 1
23
+ },
24
+ "x-prompt": "What should the views be called?"
25
+ },
26
+ "resource": {
27
+ "type": "string",
28
+ "description": "API resource path segment -- fetches /api/<resource> (list), /api/<resource>/:id (load one), POST /api/<resource> (create), PUT /api/<resource>/:id (update)."
29
+ },
30
+ "route": {
31
+ "type": "string",
32
+ "description": "List route path added to router/index.ts, e.g. /regions. The edit/create route is added as `${route}/:id` (visiting `${route}/new` creates)."
33
+ },
34
+ "fields": {
35
+ "type": "string",
36
+ "description": "JSON array of fields, in display/form order -- e.g. '[{\"key\":\"name\",\"label\":\"Name\"},{\"key\":\"active\",\"label\":\"Active\",\"type\":\"checkbox\"}]'. Each item: { key, label, type?: \"text\"|\"checkbox\" (default \"text\"), required?: boolean (default true for \"text\", ignored for \"checkbox\") }. A plain array is also accepted when this generator is invoked programmatically."
37
+ },
38
+ "heading": {
39
+ "type": "string",
40
+ "description": "List page heading. Defaults to the view name, title-cased."
41
+ },
42
+ "singularLabel": {
43
+ "type": "string",
44
+ "description": "Singular label used in \"Create <label>\"/\"Edit <label>\" headings and buttons. Defaults to --heading (override for irregular plurals, e.g. --heading=Regions --singularLabel=Region)."
45
+ },
46
+ "requiresAuth": {
47
+ "type": "boolean",
48
+ "description": "Whether the generated routes require authentication.",
49
+ "default": true
50
+ }
51
+ },
52
+ "required": ["project", "name", "resource", "route", "fields"],
53
+ "additionalProperties": false
54
+ }
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<void>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const path = require("path");
7
+ const vue_router_1 = require("../../utils/vue-router");
8
+ const vue_components_1 = require("../vue-components/vue-components");
9
+ // Nx's own CLI option coercion (coerceTypesInOptions in nx/src/utils/params)
10
+ // only knows how to split an array-typed option on commas -- it has no JSON
11
+ // support, so a real `"type": "array"` schema for --fields silently mangles a
12
+ // JSON array into garbage fragments when invoked from the actual CLI (only
13
+ // programmatic callers, like this generator's own unit tests, ever pass a
14
+ // real array). --fields is `"type": "string"` in schema.json specifically so
15
+ // Nx leaves it alone, and this generator parses the JSON itself.
16
+ function parseFields(fields) {
17
+ const parsed = typeof fields === 'string' ? JSON.parse(fields) : fields;
18
+ if (!Array.isArray(parsed) || parsed.length === 0) {
19
+ throw new Error('--fields must be a non-empty JSON array of { key, label, type?, required? } objects.');
20
+ }
21
+ return parsed;
22
+ }
23
+ function normalizeOptions(host, options) {
24
+ var _a, _b, _c;
25
+ const { root: projectRoot } = (0, devkit_1.readProjectConfiguration)(host, options.project);
26
+ const className = (0, devkit_1.names)(options.name).className;
27
+ // className is PascalCase (e.g. "Regions") -- space it out for a readable
28
+ // default heading ("Regions").
29
+ const heading = (_a = options.heading) !== null && _a !== void 0 ? _a : className.replace(/([A-Z])/g, ' $1').trim();
30
+ return Object.assign(Object.assign({}, options), { projectRoot, fields: parseFields(options.fields), listViewFileName: `${className}ListView`, editViewFileName: `${className}EditView`, heading, singularLabel: (_b = options.singularLabel) !== null && _b !== void 0 ? _b : heading, requiresAuth: (_c = options.requiresAuth) !== null && _c !== void 0 ? _c : true });
31
+ }
32
+ function default_1(host, options) {
33
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
34
+ const normalizedOptions = normalizeOptions(host, options);
35
+ // Idempotent -- ensures WorkspaceTable exists even in a project scaffolded
36
+ // before vue-components carried it.
37
+ yield (0, vue_components_1.default)(host);
38
+ (0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), normalizedOptions.projectRoot, Object.assign(Object.assign({}, normalizedOptions), { goaImportPath: (0, vue_components_1.vueComponentsImportPath)(host), tmpl: '' }));
39
+ // The edit route also serves create: visiting `${route}/new` matches the
40
+ // same `:id` param (id === 'new'), same convention the real reference
41
+ // implementation this was modeled on uses -- no separate create route.
42
+ (0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
43
+ path: `${normalizedOptions.route}/:id`,
44
+ componentImportPath: `../views/${normalizedOptions.editViewFileName}.vue`,
45
+ requiresAuth: normalizedOptions.requiresAuth,
46
+ });
47
+ (0, vue_router_1.insertVueRoute)(host, normalizedOptions.projectRoot, normalizedOptions.project, {
48
+ path: normalizedOptions.route,
49
+ componentImportPath: `../views/${normalizedOptions.listViewFileName}.vue`,
50
+ requiresAuth: normalizedOptions.requiresAuth,
51
+ });
52
+ yield (0, devkit_1.formatFiles)(host);
53
+ });
54
+ }
55
+ //# sourceMappingURL=vue-admin-crud.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue-admin-crud.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-admin-crud/vue-admin-crud.ts"],"names":[],"mappings":";;AAkDA,4BAiCC;;AAnFD,uCAMoB;AACpB,6BAA6B;AAC7B,uDAAwD;AACxD,qEAE0C;AAG1C,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,WAAW,CAAC,MAAwB;IAC3C,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,sFAAsF,CACvF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAe;;IACnD,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E,MAAM,SAAS,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IAChD,0EAA0E;IAC1E,+BAA+B;IAC/B,MAAM,OAAO,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/E,uCACK,OAAO,KACV,WAAW,EACX,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EACnC,gBAAgB,EAAE,GAAG,SAAS,UAAU,EACxC,gBAAgB,EAAE,GAAG,SAAS,UAAU,EACxC,OAAO,EACP,aAAa,EAAE,MAAA,OAAO,CAAC,aAAa,mCAAI,OAAO,EAC/C,YAAY,EAAE,MAAA,OAAO,CAAC,YAAY,mCAAI,IAAI,IAC1C;AACJ,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,2EAA2E;QAC3E,oCAAoC;QACpC,MAAM,IAAA,wBAAsB,EAAC,IAAI,CAAC,CAAC;QAEnC,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAC7B,iBAAiB,CAAC,WAAW,kCAExB,iBAAiB,KACpB,aAAa,EAAE,IAAA,wCAAuB,EAAC,IAAI,CAAC,EAC5C,IAAI,EAAE,EAAE,IAEX,CAAC;QAEF,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,IAAA,2BAAc,EAAC,IAAI,EAAE,iBAAiB,CAAC,WAAW,EAAE,iBAAiB,CAAC,OAAO,EAAE;YAC7E,IAAI,EAAE,GAAG,iBAAiB,CAAC,KAAK,MAAM;YACtC,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,gBAAgB,MAAM;YACzE,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CAAC,CAAC;QACH,IAAA,2BAAc,EAAC,IAAI,EAAE,iBAAiB,CAAC,WAAW,EAAE,iBAAiB,CAAC,OAAO,EAAE;YAC7E,IAAI,EAAE,iBAAiB,CAAC,KAAK;YAC7B,mBAAmB,EAAE,YAAY,iBAAiB,CAAC,gBAAgB,MAAM;YACzE,YAAY,EAAE,iBAAiB,CAAC,YAAY;SAC7C,CAAC,CAAC;QAEH,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CAAA"}