@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/backend/entities/module-action.entity.d.ts +30 -0
- package/dist/backend/entities/module-action.entity.d.ts.map +1 -0
- package/dist/backend/entities/module-action.entity.js +100 -0
- package/dist/backend/entities/module-action.entity.js.map +1 -0
- package/dist/backend/index.d.ts +85 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +57 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +110 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +77 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +19 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +32 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/admin-actions-reconciler.d.ts +49 -0
- package/dist/backend/services/admin-actions-reconciler.d.ts.map +1 -0
- package/dist/backend/services/admin-actions-reconciler.js +78 -0
- package/dist/backend/services/admin-actions-reconciler.js.map +1 -0
- package/dist/backend/services/admin-actions-service.d.ts +136 -0
- package/dist/backend/services/admin-actions-service.d.ts.map +1 -0
- package/dist/backend/services/admin-actions-service.js +168 -0
- package/dist/backend/services/admin-actions-service.js.map +1 -0
- package/dist/manifest.d.ts +196 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +85 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260507T142354_admin_actions_init.d.ts +31 -0
- package/dist/migrations/20260507T142354_admin_actions_init.d.ts.map +1 -0
- package/dist/migrations/20260507T142354_admin_actions_init.js +55 -0
- package/dist/migrations/20260507T142354_admin_actions_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/admin-actions.md +164 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +68 -0
package/dist/manifest.js
ADDED
|
@@ -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
package/i18n/pl.json
ADDED
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
|
+
}
|