@endora-commerce/mod-admin-actions 0.100.0

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/backend/entities/module-action.entity.d.ts +30 -0
  4. package/dist/backend/entities/module-action.entity.d.ts.map +1 -0
  5. package/dist/backend/entities/module-action.entity.js +100 -0
  6. package/dist/backend/entities/module-action.entity.js.map +1 -0
  7. package/dist/backend/index.d.ts +85 -0
  8. package/dist/backend/index.d.ts.map +1 -0
  9. package/dist/backend/index.js +57 -0
  10. package/dist/backend/index.js.map +1 -0
  11. package/dist/backend/plugin.d.ts +110 -0
  12. package/dist/backend/plugin.d.ts.map +1 -0
  13. package/dist/backend/plugin.js +77 -0
  14. package/dist/backend/plugin.js.map +1 -0
  15. package/dist/backend/routes.admin.d.ts +19 -0
  16. package/dist/backend/routes.admin.d.ts.map +1 -0
  17. package/dist/backend/routes.admin.js +32 -0
  18. package/dist/backend/routes.admin.js.map +1 -0
  19. package/dist/backend/services/admin-actions-reconciler.d.ts +49 -0
  20. package/dist/backend/services/admin-actions-reconciler.d.ts.map +1 -0
  21. package/dist/backend/services/admin-actions-reconciler.js +78 -0
  22. package/dist/backend/services/admin-actions-reconciler.js.map +1 -0
  23. package/dist/backend/services/admin-actions-service.d.ts +136 -0
  24. package/dist/backend/services/admin-actions-service.d.ts.map +1 -0
  25. package/dist/backend/services/admin-actions-service.js +168 -0
  26. package/dist/backend/services/admin-actions-service.js.map +1 -0
  27. package/dist/manifest.d.ts +196 -0
  28. package/dist/manifest.d.ts.map +1 -0
  29. package/dist/manifest.js +85 -0
  30. package/dist/manifest.js.map +1 -0
  31. package/dist/migrations/20260507T142354_admin_actions_init.d.ts +31 -0
  32. package/dist/migrations/20260507T142354_admin_actions_init.d.ts.map +1 -0
  33. package/dist/migrations/20260507T142354_admin_actions_init.js +55 -0
  34. package/dist/migrations/20260507T142354_admin_actions_init.js.map +1 -0
  35. package/dist/migrations/index.d.ts +27 -0
  36. package/dist/migrations/index.d.ts.map +1 -0
  37. package/dist/migrations/index.js +29 -0
  38. package/dist/migrations/index.js.map +1 -0
  39. package/docs/admin-actions.md +164 -0
  40. package/i18n/en.json +5 -0
  41. package/i18n/pl.json +5 -0
  42. package/package.json +68 -0
@@ -0,0 +1,85 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * Admin Command Palette Actions Registry — feature 020.
4
+ *
5
+ * Owns the `module_actions` table, the install-time reconciler that
6
+ * ingests every module's manifest `actions` array, the in-process
7
+ * cache, and the read API consumed by the admin SPA's command palette.
8
+ * Itself declares no actions — this module is platform plumbing, not
9
+ * a place to surface UI features.
10
+ *
11
+ * Depends on `_lifecycle` because every module's action install / hard-
12
+ * uninstall is driven by the lifecycle orchestrator's hook surface, and
13
+ * on `_i18n` because action labels and descriptions are resolved
14
+ * through the existing translation-bundle resolver.
15
+ */
16
+ export const manifest = defineModuleManifest({
17
+ id: 'admin_actions',
18
+ name: 'Admin Command Palette Actions',
19
+ description: 'Module-contributed action registry that surfaces declared actions in the admin command palette.',
20
+ version: '1.0.0',
21
+ // `auth` owns the `requireAdmin` port; `admin_roles` owns the permission
22
+ // service the palette filters entries with. Feature 072 made both container
23
+ // resolutions rather than constructor arguments.
24
+ dependencies: ['_i18n', '_lifecycle', 'auth', 'admin_roles'],
25
+ i18n: { bundlesDir: 'i18n' },
26
+ docs: { dir: 'docs' },
27
+ settings: {
28
+ moduleCode: 'admin_actions',
29
+ groups: [{ code: 'admin_actions', name: 'Command palette' }],
30
+ settings: [
31
+ {
32
+ // Feature 073 — the operator's activation control. Platform-wide.
33
+ code: 'admin_actions.enabled',
34
+ name: 'Command palette enabled',
35
+ description: 'Switches the admin command palette on or off: the action registry every module contributes to and the read API behind ⌘K. Switched off, the admin is navigated through the sidebar alone; nothing is dropped — the registered actions stay in the database and the palette answers again exactly as before when you switch it back on.',
36
+ groupCode: 'admin_actions',
37
+ valueType: 'boolean',
38
+ defaultValue: true,
39
+ },
40
+ ],
41
+ },
42
+ // Feature 073, Amendment A1 (Constitution XVII) — deactivatable. What this
43
+ // module gates is *discoverability*: with it off the sidebar still navigates
44
+ // the whole admin, so losing ⌘K is a degradation rather than the loss of a
45
+ // capability. It is also the only module in the declaring set with no external
46
+ // manifest dependent at all — nothing fails closed behind it — so the flag was
47
+ // protecting a convenience, which is not what `nonDeactivatable` is for.
48
+ activation: { settingCode: 'admin_actions.enabled', default: true },
49
+ });
50
+ /**
51
+ * `module_actions` follows the manifest set — feature 080, T036a / D-159.
52
+ *
53
+ * `_i18n/manifest.ts` states the shape's reasoning in full; this module is the
54
+ * second instance of it, projecting every other module's `manifest.actions`
55
+ * array instead of its `i18n` block.
56
+ *
57
+ * **It is not gated on this module's own activation, and that is a ruling**
58
+ * (D-159 §9, the owner, 2026-08-22). `composition.ts` used to forward the
59
+ * reconciler as a lazily-resolved port, so an operator who had switched the
60
+ * command palette off could not install an *unrelated* module — the install
61
+ * aborted on a `MODULE_DISABLED` from a discovery surface that has nothing to
62
+ * do with it. That was the better of the only two options then on the table,
63
+ * the other being a backend that would not start; the third, which this takes,
64
+ * is that a projection of manifest data is written whether or not anything is
65
+ * serving it. The rows are inert while the palette is off and the palette
66
+ * answers from them, unchanged, the moment it is switched back on.
67
+ */
68
+ export const lifecycleParticipant = {
69
+ async onModuleInstalled({ moduleId, manifest: installed, em }) {
70
+ const { AdminActionsReconciler } = await import('./backend/services/admin-actions-reconciler.js');
71
+ await new AdminActionsReconciler({ em: () => em }).installForModule({
72
+ moduleId,
73
+ // Unconditional, including for the empty array: `installForModule`
74
+ // upserts *and prunes*, so a module that has dropped its last action
75
+ // needs the call in order for its last row to go.
76
+ actions: installed.actions ?? [],
77
+ em,
78
+ });
79
+ },
80
+ async onModuleHardUninstalled({ moduleId, em }) {
81
+ const { AdminActionsReconciler } = await import('./backend/services/admin-actions-reconciler.js');
82
+ await new AdminActionsReconciler({ em: () => em }).removeForModule({ moduleId, em });
83
+ },
84
+ };
85
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAmC,MAAM,4BAA4B,CAAC;AAEnG;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,eAAe;IACnB,IAAI,EAAE,+BAA+B;IACrC,WAAW,EACT,iGAAiG;IACnG,OAAO,EAAE,OAAO;IAChB,yEAAyE;IACzE,4EAA4E;IAC5E,iDAAiD;IACjD,YAAY,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,CAAC;IAC5D,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,QAAQ,EAAE;QACR,UAAU,EAAE,eAAe;QAC3B,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAC5D,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,uBAAuB;gBAC7B,IAAI,EAAE,yBAAyB;gBAC/B,WAAW,EACT,wUAAwU;gBAC1U,SAAS,EAAE,eAAe;gBAC1B,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,2EAA2E;IAC3E,6EAA6E;IAC7E,2EAA2E;IAC3E,+EAA+E;IAC/E,+EAA+E;IAC/E,yEAAyE;IACzE,UAAU,EAAE,EAAE,WAAW,EAAE,uBAAuB,EAAE,OAAO,EAAE,IAAI,EAAE;CACpE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA8C;IAC7E,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE;QAC3D,MAAM,EAAE,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAC7C,gDAAgD,CACjD,CAAC;QACF,MAAM,IAAI,sBAAsB,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC;YAClE,QAAQ;YACR,mEAAmE;YACnE,qEAAqE;YACrE,kDAAkD;YAClD,OAAO,EAAE,SAAS,CAAC,OAAO,IAAI,EAAE;YAChC,EAAE;SACH,CAAC,CAAC;IACL,CAAC;IACD,KAAK,CAAC,uBAAuB,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5C,MAAM,EAAE,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAC7C,gDAAgD,CACjD,CAAC;QACF,MAAM,IAAI,sBAAsB,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACvF,CAAC;CACF,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Module-Contributed Admin Actions — feature 020 / data-model.md.
4
+ *
5
+ * Schema changes (one atomic migration):
6
+ * - new sequence `module_actions_version_seq` — monotonically-
7
+ * increasing version vector consumed by the registry's HTTP cache
8
+ * for `meta.registryVersion` diagnostics.
9
+ * - new table `module_actions` — one row per `(module_id, action_id)`.
10
+ * Stores the action's i18n keys, icon, target route, optional
11
+ * required-permission code, optional keywords array (JSONB), and
12
+ * ordering weight. Reconciler at module-install time UPSERTs every
13
+ * declared action and prunes any rows the new manifest no longer
14
+ * declares.
15
+ * - btree index `idx_module_actions_weight_label_key` to support the
16
+ * visibility query's ORDER BY (weight, label_key) when the cache
17
+ * misses.
18
+ *
19
+ * No FK on `module_actions.module_id`: same rationale as feature 019's
20
+ * `translation_bundles` — modules are filesystem-driven (feature 018)
21
+ * and `module_registrations` is the registry of record; coupling
22
+ * lifecycle ordering through a FK would conflict with how the
23
+ * orchestrator runs its install phases. Cleanup is enforced by the
24
+ * lifecycle hard-uninstall hook (admin-actions reconciler), not by
25
+ * ON DELETE CASCADE.
26
+ */
27
+ export declare class Migration20260507T142354AdminActionsInit extends Migration {
28
+ up(): Promise<void>;
29
+ down(): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=20260507T142354_admin_actions_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260507T142354_admin_actions_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260507T142354_admin_actions_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,wCAAyC,SAAQ,SAAS;IACtD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}
@@ -0,0 +1,55 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Module-Contributed Admin Actions — feature 020 / data-model.md.
4
+ *
5
+ * Schema changes (one atomic migration):
6
+ * - new sequence `module_actions_version_seq` — monotonically-
7
+ * increasing version vector consumed by the registry's HTTP cache
8
+ * for `meta.registryVersion` diagnostics.
9
+ * - new table `module_actions` — one row per `(module_id, action_id)`.
10
+ * Stores the action's i18n keys, icon, target route, optional
11
+ * required-permission code, optional keywords array (JSONB), and
12
+ * ordering weight. Reconciler at module-install time UPSERTs every
13
+ * declared action and prunes any rows the new manifest no longer
14
+ * declares.
15
+ * - btree index `idx_module_actions_weight_label_key` to support the
16
+ * visibility query's ORDER BY (weight, label_key) when the cache
17
+ * misses.
18
+ *
19
+ * No FK on `module_actions.module_id`: same rationale as feature 019's
20
+ * `translation_bundles` — modules are filesystem-driven (feature 018)
21
+ * and `module_registrations` is the registry of record; coupling
22
+ * lifecycle ordering through a FK would conflict with how the
23
+ * orchestrator runs its install phases. Cleanup is enforced by the
24
+ * lifecycle hard-uninstall hook (admin-actions reconciler), not by
25
+ * ON DELETE CASCADE.
26
+ */
27
+ export class Migration20260507T142354AdminActionsInit extends Migration {
28
+ async up() {
29
+ this.addSql(`create sequence "module_actions_version_seq" as bigint;`);
30
+ this.addSql(`
31
+ create table "module_actions" (
32
+ "module_id" varchar(64) not null,
33
+ "action_id" varchar(64) not null,
34
+ "label_key" varchar(255) not null,
35
+ "description_key" varchar(255) null,
36
+ "icon" varchar(64) not null,
37
+ "target_route" varchar(255) not null,
38
+ "required_permission" varchar(64) null,
39
+ "keywords" jsonb not null default '[]'::jsonb,
40
+ "weight" integer not null default 100,
41
+ "version" bigint not null default nextval('module_actions_version_seq'),
42
+ "installed_at" timestamptz not null default now(),
43
+ "updated_at" timestamptz not null default now(),
44
+ constraint "module_actions_pkey" primary key ("module_id", "action_id")
45
+ );
46
+ `);
47
+ this.addSql(`alter sequence "module_actions_version_seq" owned by "module_actions"."version";`);
48
+ this.addSql(`create index "idx_module_actions_weight_label_key" on "module_actions" ("weight", "label_key");`);
49
+ }
50
+ async down() {
51
+ this.addSql(`drop table if exists "module_actions";`);
52
+ this.addSql(`drop sequence if exists "module_actions_version_seq";`);
53
+ }
54
+ }
55
+ //# sourceMappingURL=20260507T142354_admin_actions_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260507T142354_admin_actions_init.js","sourceRoot":"","sources":["../../src/migrations/20260507T142354_admin_actions_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAO,wCAAyC,SAAQ,SAAS;IAC5D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,yDAAyD,CAAC,CAAC;QAEvE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CACT,kFAAkF,CACnF,CAAC;QAEF,IAAI,CAAC,MAAM,CACT,iGAAiG,CAClG,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,wCAAwC,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,uDAAuD,CAAC,CAAC;IACvE,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which orders **this module's own** migrations
10
+ * and nothing else (feature 081). Where the block sits relative to every other
11
+ * module's is decided by the manifest `dependencies` graph.
12
+ *
13
+ * The **named** exports stay, and the asymmetry with `./backend` — which
14
+ * publishes an array and no entity class by name (D-168) — is deliberate.
15
+ * `db/migrations-registry.generated.ts` imports each class by name from this
16
+ * specifier, and a migration class name is contract in a way an entity class
17
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom
22
+ * is a query against a table nobody created.
23
+ */
24
+ import { Migration20260507T142354AdminActionsInit } from './20260507T142354_admin_actions_init.js';
25
+ export declare const migrations: (typeof Migration20260507T142354AdminActionsInit)[];
26
+ export { Migration20260507T142354AdminActionsInit, };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AAEnG,eAAO,MAAM,UAAU,qDAEtB,CAAC;AAEF,OAAO,EACL,wCAAwC,GACzC,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which orders **this module's own** migrations
10
+ * and nothing else (feature 081). Where the block sits relative to every other
11
+ * module's is decided by the manifest `dependencies` graph.
12
+ *
13
+ * The **named** exports stay, and the asymmetry with `./backend` — which
14
+ * publishes an array and no entity class by name (D-168) — is deliberate.
15
+ * `db/migrations-registry.generated.ts` imports each class by name from this
16
+ * specifier, and a migration class name is contract in a way an entity class
17
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom
22
+ * is a query against a table nobody created.
23
+ */
24
+ import { Migration20260507T142354AdminActionsInit } from './20260507T142354_admin_actions_init.js';
25
+ export const migrations = [
26
+ Migration20260507T142354AdminActionsInit,
27
+ ];
28
+ export { Migration20260507T142354AdminActionsInit, };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AAEnG,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,wCAAwC;CACzC,CAAC;AAEF,OAAO,EACL,wCAAwC,GACzC,CAAC"}
@@ -0,0 +1,164 @@
1
+ ---
2
+ title: Admin Command Palette Actions
3
+ description: Module-contributed action registry surfaced in the Admin Command Palette (⌘K Actions group)
4
+ ---
5
+
6
+ # Admin Command Palette Actions
7
+
8
+ A registry-backed contribution point that lets every backend module add action buttons to
9
+ the Admin UI's command palette (the `⌘K` / `Ctrl+K` modal — what the operator sees as the
10
+ **Actions** group). Two actions ship hardcoded today (*New product*, *Import products*); those,
11
+ and every future action, are declared once in their owning module's manifest and surfaced
12
+ through this registry.
13
+
14
+ The platform side lives at `packages/modules/admin_actions/` and the admin runtime at
15
+ `admin/src/lib/admin-actions/`.
16
+
17
+ ## What a module declares
18
+
19
+ A module's `manifest.ts` may declare zero or more actions inline alongside its existing
20
+ `settings` and `i18n` fields:
21
+
22
+ ```ts
23
+ import { defineModuleManifest } from '@endora-commerce/contracts';
24
+
25
+ export const manifest = defineModuleManifest({
26
+ id: 'catalog',
27
+ name: 'Catalog',
28
+ version: '1.4.0',
29
+ dependencies: [],
30
+ i18n: { bundlesDir: 'i18n' },
31
+ actions: [
32
+ {
33
+ id: 'new-product',
34
+ labelKey: 'catalog.actions.newProduct.label',
35
+ descriptionKey: 'catalog.actions.newProduct.description',
36
+ icon: 'Plus',
37
+ targetRoute: '/catalog/products/new',
38
+ requiredPermission: 'catalog:write',
39
+ keywords: ['product', 'new', 'add', 'create', 'produkt', 'nowy', 'dodaj'],
40
+ weight: 100,
41
+ },
42
+ ],
43
+ });
44
+ ```
45
+
46
+ Each entry MUST carry a stable `id`, a translatable `labelKey`, an `icon` from the closed
47
+ allowlist, and a `targetRoute`. Optional fields are `descriptionKey`, `requiredPermission`,
48
+ `keywords` (up to 10), and `weight` (default 100).
49
+
50
+ `requiredPermission` is optional in the schema and all but mandatory in practice: it must be
51
+ **the code the backend enforces on the route behind `targetRoute`**, so the palette never
52
+ advertises a 403 and never hides a screen from an operator entitled to open it. Both failures
53
+ happened — `settings/open-settings` shipped with no code at all against a `settings:read`
54
+ route, and `inventory/open-inventory` declared `catalog:write` against an `orders:read` one —
55
+ and the permission inventory could not see either, because it sweeps whether a code is
56
+ *enforced somewhere*, not whether it is enforced *here*.
57
+ `pnpm --filter backend run check:action-route-permissions` compares the two, resolving the SPA
58
+ `targetRoute` to the admin API route that gates it. Leave the field unset only when the
59
+ destination genuinely has no gate; where the screen is read-gated but the action's label
60
+ promises a write, the field cannot say both, and the disagreement is recorded in that check's
61
+ ledger rather than guessed at.
62
+
63
+ Within-module `id` uniqueness is enforced by the manifest's Zod schema — installing a
64
+ manifest with two actions sharing an id fails the install with a clear, indexed error.
65
+
66
+ ## Public surface
67
+
68
+ | Verb + Path | Purpose |
69
+ | --- | --- |
70
+ | `GET /api/v1/admin/admin-actions?language=<en\|pl>` | Returns the operator-visible action list, already filtered by the operator's permissions and the module's installed state, sorted by `(weight, locale-aware label)`, with labels and descriptions resolved into the requested language (falling back to English then to the raw key, identical to the Admin UI i18n fallback chain). Permission: any authenticated admin. |
71
+
72
+ The response carries a `meta.registryVersion` field — `MAX(version)` over the visible rows
73
+ — useful for diagnostics. The admin SPA does not poll on it; refreshes are driven by the
74
+ operator's language flip and on-mount fetch.
75
+
76
+ ## How an operator sees actions
77
+
78
+ 1. The operator opens the Admin UI and presses `⌘K` (or `Ctrl+K` on Windows / Linux).
79
+ 2. The palette renders two groups: **Navigate** (static jump targets) and **Actions**.
80
+ 3. The Actions group shows every action whose owning module is installed AND whose
81
+ `requiredPermission` (if any) the operator's role grants. The wildcard `*` permission
82
+ held by `platform_admin` satisfies every action.
83
+ 4. Typing in the search box filters across **both** groups by case-insensitive,
84
+ diacritic-insensitive substring match against the row's label, description, and
85
+ keywords. Polish operators can type `latwy` to match `łatwy`, English operators can
86
+ type `import` to match `Importuj produkty`, etc.
87
+ 5. Clicking a row or pressing `Enter` navigates to the action's `targetRoute` and closes
88
+ the palette.
89
+
90
+ When no action is visible to the operator (rare; only with a no-permission role and no
91
+ modules contributing permissionless actions), the **Actions** group is hidden entirely.
92
+
93
+ ## Lifecycle integration
94
+
95
+ The orchestrator install path runs `module_actions` reconciliation between the i18n
96
+ bundle install and the module's own install hook. The reconciler UPSERTs every declared
97
+ action and prunes any rows the new manifest no longer declares — install order, version
98
+ upgrades, and action removals are all idempotent. On hard-uninstall (`module:uninstall
99
+ --hard`) the reconciler deletes every action row owned by the module before the i18n
100
+ bundle removal step.
101
+
102
+ State is persisted in `module_actions` (composite PK `(module_id, action_id)`); soft-
103
+ uninstall (state → `disabled`) does NOT delete rows — it relies on the visibility read
104
+ filtering every row whose module is not effectively present, which hides the actions
105
+ while preserving them for re-enable.
106
+
107
+ That filter asks the kernel's effective-state combiner, through a probe the composition
108
+ root contributes, and it asks it for **both** presence axes: the deployment's
109
+ `module_registrations` state and the operator's activation Setting. It used to ask only
110
+ the second that way and join `module_registrations.state = 'installed'` for the first,
111
+ which meant the palette and the route gates read one question out of two sources —
112
+ disagreeing for the length of every `registryCache.refreshFromDb`, so a palette could
113
+ advertise an action whose route answered 503 and hide one the route would still serve.
114
+ The registry table is still the record for the platform axis; the palette simply no
115
+ longer reads it behind the platform's back.
116
+
117
+ ## Storage shape
118
+
119
+ | Column | Type | Notes |
120
+ | --- | --- | --- |
121
+ | `module_id` | `varchar(64)` | Part of PK. |
122
+ | `action_id` | `varchar(64)` | Part of PK. |
123
+ | `label_key` | `varchar(255)` | i18n key resolved at read time. |
124
+ | `description_key` | `varchar(255) NULL` | Optional. |
125
+ | `icon` | `varchar(64)` | One of the closed-allowlist names. |
126
+ | `target_route` | `varchar(255)` | Admin route. |
127
+ | `required_permission` | `varchar(64) NULL` | Permission code, any notation. |
128
+ | `keywords` | `jsonb` | Array of strings. |
129
+ | `weight` | `integer` | Sort key (default 100). |
130
+ | `version` | `bigint` | Per-row sequence; bumped on every UPSERT. |
131
+ | `installed_at`, `updated_at` | `timestamptz` | Row metadata. |
132
+
133
+ There is no foreign key on `module_id` — modules are filesystem-driven and
134
+ `module_registrations` is the registry of record. Cleanup is enforced by the
135
+ hard-uninstall path of the reconciler, mirroring the `translation_bundles`
136
+ choice.
137
+
138
+ ## Recommended weight bands
139
+
140
+ Weights are advisory but reviewers expect new actions to land in the appropriate band:
141
+
142
+ | Band | Use case |
143
+ | --- | --- |
144
+ | 0–99 | Reserved for the platform shell. |
145
+ | 100–199 | Primary creation (e.g., New product, New page, New post). |
146
+ | 200–299 | Secondary creation / configuration entry points. |
147
+ | 300–399 | Workflow / inbox actions. |
148
+ | 400–499 | Less-frequent navigation / utilities. |
149
+ | ≥ 500 | Rarely-used actions; sink to the bottom. |
150
+
151
+ ## Icon allowlist
152
+
153
+ Allowed icon names are an enum in `packages/contracts/src/admin-actions.ts`. Adding a new
154
+ icon is a one-line PR that edits both the enum and the admin's `icon-map.ts`.
155
+
156
+ ## v1 seed set
157
+
158
+ The initial release ships ten actions across nine modules: `catalog/new-product`,
159
+ `import_export/import-products`, `import_export/open-import-export-center`,
160
+ `inventory/open-inventory`, `quote_requests/open-rfq-inbox`, `cms/new-page`,
161
+ `blog/new-post`, `megamenu/edit-megamenu`, `sales_channels/new-sales-channel`,
162
+ `settings/open-settings`. Eight further candidates from the spec were deferred until
163
+ their target admin pages exist (Adjust stock, New draft order, Find order by number,
164
+ New customer, New price list, New promotion, Upload asset, New category).
package/i18n/en.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "palette.actionsHeader": "Actions",
3
+ "palette.empty": "No actions available",
4
+ "palette.searchPlaceholder": "Type to search actions…"
5
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "palette.actionsHeader": "Akcje",
3
+ "palette.empty": "Brak dostępnych akcji",
4
+ "palette.searchPlaceholder": "Wpisz, aby wyszukać akcje…"
5
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@endora-commerce/mod-admin-actions",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Module-contributed action registry that surfaces declared actions in the admin command palette.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "admin_actions"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/admin_actions"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "i18n",
38
+ "docs"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22.18.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@mikro-orm/core": "^6",
45
+ "@mikro-orm/migrations": "^6",
46
+ "@mikro-orm/postgresql": "^6",
47
+ "fastify": "^5",
48
+ "@endora-commerce/contracts": "0.100.0",
49
+ "@endora-commerce/platform": "0.100.0"
50
+ },
51
+ "devDependencies": {
52
+ "@mikro-orm/core": "^6.6.13",
53
+ "@mikro-orm/migrations": "^6.6.13",
54
+ "@mikro-orm/postgresql": "^6.6.13",
55
+ "@types/node": "^22.9.0",
56
+ "fastify": "^5.12.5",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.1.11",
59
+ "@endora-commerce/contracts": "0.100.0",
60
+ "@endora-commerce/platform": "0.100.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.build.json",
64
+ "typecheck": "tsc -p tsconfig.json",
65
+ "lint": "eslint src",
66
+ "test": "vitest run"
67
+ }
68
+ }