@flowdular/sdk 0.3.1 → 0.3.2
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/.ai/platform-capabilities.md +2 -0
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
- package/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
- package/.ai/references/catalog/module.json +11 -1
- package/.ai/references/catalog/package.json +2 -2
- package/.ai/references/catalog/spec/module.yaml +25 -4
- package/.ai/references/catalog/src/agent/tools.ts +19 -10
- package/.ai/references/catalog/src/api/endpoints.ts +150 -10
- package/.ai/references/catalog/src/api/list-cursor.ts +83 -0
- package/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
- package/.ai/references/catalog/src/client/api.ts +124 -36
- package/.ai/references/catalog/src/client/contribution.tsrx +5 -0
- package/.ai/references/catalog/src/client/state.ts +169 -3
- package/.ai/references/catalog/src/domain/lists.ts +7 -0
- package/.ai/references/catalog/src/domain/types.ts +20 -0
- package/.ai/references/catalog/src/platform.ts +20 -0
- package/.ai/references/catalog/src/services/catalog-service.ts +143 -8
- package/.ai/references/catalog/src/services/database-repository.ts +104 -17
- package/.ai/references/catalog/src/services/item-export.ts +81 -0
- package/.ai/references/catalog/src/services/migration.ts +27 -1
- package/.ai/references/catalog/src/services/repository.ts +31 -2
- package/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
- package/.ai/references/catalog/tests/client-state.test.ts +124 -0
- package/.ai/references/catalog/tests/endpoints.test.ts +269 -0
- package/.ai/references/catalog/tests/export.test.ts +134 -0
- package/.ai/references/catalog/tests/idempotency.test.ts +15 -14
- package/.ai/references/catalog/tests/list.test.ts +217 -0
- package/.ai/references/catalog/tests/migrations.test.ts +58 -2
- package/.ai/references/catalog/tests/module.test.ts +2 -1
- package/.ai/references/catalog/tests/support/database.ts +14 -0
- package/.ai/references/catalog/translations/en.json +35 -4
- package/.ai/references/catalog/translations/pl.json +35 -4
- package/.ai/references/catalog.provenance.json +34 -26
- package/README.md +1 -1
- package/assets/flowdular-banner.webp +0 -0
- package/modules/access/module.json +1 -1
- package/modules/access/spec/module.yaml +1 -1
- package/modules/agents/module.json +1 -1
- package/modules/agents/spec/module.yaml +1 -1
- package/modules/approvals/module.json +1 -1
- package/modules/approvals/spec/module.yaml +1 -1
- package/modules/audit/module.json +1 -1
- package/modules/audit/spec/module.yaml +1 -1
- package/modules/auth/module.json +2 -2
- package/modules/auth/package.json +1 -1
- package/modules/auth/spec/module.yaml +2 -2
- package/modules/auth/src/middleware/authentication.ts +5 -1
- package/modules/auth/src/services/auth-service.ts +26 -0
- package/modules/auth/src/services/database-repository.ts +9 -0
- package/modules/auth/src/services/repository.ts +2 -0
- package/modules/automations/module.json +1 -1
- package/modules/automations/spec/module.yaml +1 -1
- package/modules/connectors/module.json +1 -1
- package/modules/connectors/spec/module.yaml +1 -1
- package/modules/directory/module.json +1 -1
- package/modules/directory/spec/module.yaml +1 -1
- package/modules/documents/module.json +1 -1
- package/modules/documents/spec/module.yaml +1 -1
- package/modules/exports/module.json +1 -1
- package/modules/exports/spec/module.yaml +1 -1
- package/modules/import/module.json +1 -1
- package/modules/import/spec/module.yaml +1 -1
- package/modules/metering/module.json +1 -1
- package/modules/metering/spec/module.yaml +1 -1
- package/modules/notifications/module.json +1 -1
- package/modules/notifications/spec/module.yaml +1 -1
- package/modules/reports/module.json +1 -1
- package/modules/reports/spec/module.yaml +1 -1
- package/modules/sandbox/module.json +1 -1
- package/modules/sandbox/spec/module.yaml +1 -1
- package/modules/search/module.json +1 -1
- package/modules/search/spec/module.yaml +1 -1
- package/modules/system/migrations/0001_system_module_activations.down.sql +2 -0
- package/modules/system/migrations/0001_system_module_activations.up.sql +17 -0
- package/modules/system/module.json +3 -2
- package/modules/system/package.json +2 -2
- package/modules/system/spec/module.yaml +98 -1
- package/modules/system/src/client/ModulesView.tsrx +101 -8
- package/modules/system/src/client/api.ts +52 -11
- package/modules/system/src/client/module-columns.tsrx +32 -6
- package/modules/system/src/client/state.ts +26 -0
- package/modules/system/src/domain/modules.ts +93 -0
- package/modules/system/src/index.ts +13 -0
- package/modules/system/src/platform.ts +41 -1
- package/modules/system/src/server/capability.ts +6 -0
- package/modules/system/src/server/endpoints.ts +125 -5
- package/modules/system/src/server/index.ts +26 -0
- package/modules/system/src/server/module-catalog.ts +19 -0
- package/modules/system/src/server/runtime.ts +102 -0
- package/modules/system/src/services/database-repository.ts +91 -0
- package/modules/system/src/services/migration.ts +39 -0
- package/modules/system/src/services/module-activation-service.ts +245 -0
- package/modules/system/src/services/repository.ts +17 -0
- package/modules/system/translations/en.json +17 -1
- package/modules/system/translations/pl.json +17 -1
- package/modules/users/module.json +2 -2
- package/modules/users/package.json +1 -1
- package/modules/users/spec/module.yaml +6 -9
- package/modules/users/src/client/UsersView.tsrx +27 -13
- package/modules/users/src/client/member-columns.tsrx +13 -4
- package/modules/users/src/client/state.ts +16 -0
- package/modules/users/src/services/users-service.ts +3 -0
- package/modules/users/translations/en.json +2 -1
- package/modules/users/translations/pl.json +2 -1
- package/modules/workflows/module.json +2 -2
- package/modules/workflows/package.json +1 -1
- package/modules/workflows/spec/module.yaml +2 -2
- package/modules/workflows/src/services/database-repository.ts +93 -53
- package/package.json +1 -1
- package/packages/client/package.json +1 -0
- package/packages/client/src/ApplicationShell.tsrx +46 -5
- package/packages/client/src/index.ts +5 -0
- package/packages/client/src/shell/modules.ts +49 -0
- package/packages/client/src/shell/types.ts +3 -0
- package/packages/contracts/src/index.ts +11 -1
- package/packages/server/src/endpoint.ts +17 -1
- package/packages/server/src/index.ts +10 -0
- package/packages/server/src/module-activation.ts +69 -0
- package/assets/flowdular-banner.png +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- One row per module whose activation an owner changed for a workspace. A
|
|
2
|
+
-- composed module without a row is active; the row keeps the last decision and
|
|
3
|
+
-- who made it. The primary key is the only lookup: the snapshot reads every row
|
|
4
|
+
-- of one tenant at once.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS system_module_activations (
|
|
6
|
+
tenant_id TEXT NOT NULL,
|
|
7
|
+
module_id TEXT NOT NULL CHECK (length(module_id) BETWEEN 1 AND 128),
|
|
8
|
+
active SMALLINT NOT NULL CHECK (active IN (0, 1)),
|
|
9
|
+
changed_by TEXT NOT NULL CHECK (length(changed_by) BETWEEN 1 AND 64),
|
|
10
|
+
changed_at BIGINT NOT NULL,
|
|
11
|
+
PRIMARY KEY (tenant_id, module_id)
|
|
12
|
+
);
|
|
13
|
+
ALTER TABLE system_module_activations ENABLE ROW LEVEL SECURITY;
|
|
14
|
+
ALTER TABLE system_module_activations FORCE ROW LEVEL SECURITY;
|
|
15
|
+
CREATE POLICY system_module_activations_tenant_policy ON system_module_activations
|
|
16
|
+
USING (tenant_id = current_setting('coreloom.tenant_id', true))
|
|
17
|
+
WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
"schemaVersion": 1,
|
|
4
4
|
"id": "system.core",
|
|
5
5
|
"package": "@flowdular/module-system",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.8.0",
|
|
7
7
|
"profile": "full",
|
|
8
|
-
"capabilities": ["api", "client", "translations"],
|
|
8
|
+
"capabilities": ["api", "database", "client", "translations"],
|
|
9
9
|
"platform": {
|
|
10
10
|
"server": true,
|
|
11
11
|
"client": true
|
|
12
12
|
},
|
|
13
13
|
"dependencies": [],
|
|
14
|
+
"provides": ["system.modules.v1"],
|
|
14
15
|
"tenancy": "required",
|
|
15
16
|
"locales": ["en", "pl"],
|
|
16
17
|
"stability": "experimental",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowdular/module-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@flowdular/client": "workspace:*",
|
|
18
18
|
"@flowdular/contracts": "workspace:*",
|
|
19
|
+
"@flowdular/database": "workspace:*",
|
|
19
20
|
"@flowdular/kernel": "workspace:*",
|
|
20
21
|
"@flowdular/module-auth": "workspace:*",
|
|
21
22
|
"@flowdular/server": "workspace:*",
|
|
@@ -26,7 +27,6 @@
|
|
|
26
27
|
"yaml": "2.9.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@flowdular/database": "workspace:*",
|
|
30
30
|
"@flowdular/database-testing": "workspace:*",
|
|
31
31
|
"@tsrx/typescript-plugin": "0.3.120",
|
|
32
32
|
"@types/node": "24.13.3",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
schemaVersion: 2
|
|
2
2
|
id: system.core
|
|
3
|
-
specVersion: 0.
|
|
3
|
+
specVersion: 0.8.0
|
|
4
4
|
status: approved
|
|
5
5
|
name: System Core
|
|
6
6
|
description: Provides the application shell, platform health, module discovery, and the administration surface for module settings.
|
|
7
7
|
profile: full
|
|
8
8
|
capabilities:
|
|
9
9
|
- api
|
|
10
|
+
- database
|
|
10
11
|
- client
|
|
11
12
|
- translations
|
|
12
13
|
dependencies: []
|
|
@@ -26,6 +27,10 @@ invariants:
|
|
|
26
27
|
- Module settings are declared by their owning module, stored per tenant by auth.core, and changed only through the settings administration API served by system.core, with an audit record.
|
|
27
28
|
- system.core declares the workspace time zone as a shared tenant setting, system.core.timeZone, defaulting to UTC. Only a name the runtime zone database resolves is stored, in its canonical IANA spelling, and any module that declares a dependency on system.core reads it live through the settings runtime; a value this deployment cannot resolve reads back as UTC instead of failing the reader.
|
|
28
29
|
- A feature flag is a module setting declared with kind flag, which is a non-secret boolean with a declared default, a label, a description, and tenant scope, the only scope a flag may take. It is stored, read, validated and overridden exactly like any other setting, so a module reads it on the request path through the settings runtime at no cost beyond the settings read itself; system.core adds no flag store, no flag endpoint and no second write path.
|
|
30
|
+
- The composed module set is CLI-owned and baked at build; a module cannot be added to a running application from the UI. system.core owns per-workspace activation of the modules the application already composes, stored in the tenant table system_module_activations under forced row-level security. A composed module without a row is active.
|
|
31
|
+
- system.core, auth.core, users.core and profile.core are required and always active. A module that another active module declares as a dependency, through a declared module dependency or a required public capability, cannot be deactivated; the refusal names the active dependents. A module cannot be activated while one of its dependencies is inactive.
|
|
32
|
+
- Every activation change appends one audit event, system.module.activated or system.module.deactivated, to the auth.core trail with the module id and the actor. Repeating the current state changes nothing and writes no event.
|
|
33
|
+
- Activation is enforced in three places from one per-workspace snapshot published as the public capability system.modules.v1 and memoised for at most thirty seconds per tenant, refreshed at once by a change made through this instance. defineEndpoint answers 403 MODULE_INACTIVE for an endpoint of an inactive module in the principal's workspace, required modules exempt; the application shell hides the navigation, views, widgets and command palette entries of inactive modules; the endpoint gate never reads a tenant id from the request.
|
|
29
34
|
permissions:
|
|
30
35
|
- id: system.workspace.access
|
|
31
36
|
description: Open the authenticated ERP workspace.
|
|
@@ -44,6 +49,28 @@ dataOwnership:
|
|
|
44
49
|
- Feature modules own their navigation metadata and screen implementations.
|
|
45
50
|
- Module settings values are stored by auth.core on behalf of the declaring module; system.core owns the settings API, the per-module settings section of the Modules drawer, the Flags screen, and the settings permissions.
|
|
46
51
|
- A feature flag is owned by the module that declares it; system.core only lists and edits it, and auth.core records the change.
|
|
52
|
+
- system.core owns the per-workspace module activation rows and the system.modules.v1 capability; the composed module set stays owned by the CLI and flowdular.json.
|
|
53
|
+
entities:
|
|
54
|
+
- id: module-activation
|
|
55
|
+
name: Module activation
|
|
56
|
+
description: The last activation decision an owner made for one composed module in one workspace.
|
|
57
|
+
fields:
|
|
58
|
+
- id: moduleId
|
|
59
|
+
type: string
|
|
60
|
+
required: true
|
|
61
|
+
unique: tenant
|
|
62
|
+
maxLength: 128
|
|
63
|
+
- id: active
|
|
64
|
+
type: boolean
|
|
65
|
+
required: true
|
|
66
|
+
- id: changedBy
|
|
67
|
+
type: string
|
|
68
|
+
required: true
|
|
69
|
+
maxLength: 64
|
|
70
|
+
description: Account id of the owner who made the change.
|
|
71
|
+
- id: changedAt
|
|
72
|
+
type: datetime
|
|
73
|
+
required: true
|
|
47
74
|
acceptanceScenarios:
|
|
48
75
|
- id: SYSTEM-MULTI-VIEW-MODULE
|
|
49
76
|
given: An enabled module contributes multiple valid navigation entries and matching views.
|
|
@@ -117,15 +144,85 @@ acceptanceScenarios:
|
|
|
117
144
|
given: Enabled modules declare feature flags and a principal with system.settings.read opens the Flags screen in Administration > Modules.
|
|
118
145
|
when: The principal reads the flags and a principal with system.settings.manage turns one on for this workspace, then resets it.
|
|
119
146
|
then: Every declared flag is listed grouped by its owning module with its current value and a link to the audit trail; the override applies to that workspace alone and takes effect on the next settings read; a reset restores the declared default; each change appends one settings.flag.changed audit event carrying the module, the key, the previous and next values and the actor; and a principal without system.settings.manage is refused with 401 or 403 and changes nothing.
|
|
147
|
+
- id: SYSTEM-MODULE-ACTIVATE
|
|
148
|
+
given: An owner with system.settings.manage opens Administration > Modules, where every composed module is listed with its version, activation state and dependents and is active by default.
|
|
149
|
+
when: The owner deactivates an optional module, confirms the dialog, and later activates it again.
|
|
150
|
+
then: The module is inactive for that workspace alone and active again after the second change; each change is stored with the actor and appends one system.module.deactivated or system.module.activated audit event; a member without system.settings.manage sees the row action refused with a reason, and a request without a browser session, without the scope or without a CSRF token is refused with 401 or 403 and changes nothing.
|
|
151
|
+
- id: SYSTEM-MODULE-DEACTIVATE-REFUSED-DEPENDENCY
|
|
152
|
+
given: An active module that another active module depends on, or a required module.
|
|
153
|
+
when: An owner attempts to deactivate it.
|
|
154
|
+
then: The server refuses with 409 MODULE_HAS_ACTIVE_DEPENDENTS naming the active dependents, or 409 MODULE_REQUIRED, the screen shows the reason, nothing is stored and no audit event is written; once the dependents are inactive the deactivation is accepted, and activating a module whose dependency is inactive is refused with 409 MODULE_DEPENDENCY_INACTIVE.
|
|
155
|
+
- id: SYSTEM-MODULE-INACTIVE-ENDPOINT
|
|
156
|
+
given: A module an owner deactivated for the workspace.
|
|
157
|
+
when: A principal of that workspace calls any endpoint the module defines, or a principal of another workspace calls the same endpoint.
|
|
158
|
+
then: The first request is refused with 403 MODULE_INACTIVE after the permission check, the second is served, and an endpoint of system.core, auth.core, users.core or profile.core is never gated.
|
|
159
|
+
- id: SYSTEM-MODULE-INACTIVE-NAVIGATION
|
|
160
|
+
given: A module an owner deactivated for the workspace.
|
|
161
|
+
when: A member of that workspace opens the application shell.
|
|
162
|
+
then: The shell reads the active module ids through GET /api/system/modules/active, hides the module's navigation entries, views, workspace widgets and command palette search, returns a bookmark of one of its views to the dashboard, and shows every module when the read fails.
|
|
120
163
|
screens:
|
|
121
164
|
- id: flags
|
|
122
165
|
kind: list
|
|
123
166
|
title: Flags
|
|
124
167
|
navigationGroup: Administration
|
|
168
|
+
- id: modules
|
|
169
|
+
kind: list
|
|
170
|
+
entity: module-activation
|
|
171
|
+
title: Modules
|
|
172
|
+
navigationGroup: Administration
|
|
173
|
+
columns:
|
|
174
|
+
- moduleId
|
|
175
|
+
- active
|
|
125
176
|
actions:
|
|
177
|
+
- id: activate-module
|
|
178
|
+
entity: module-activation
|
|
179
|
+
permission: system.settings.manage
|
|
180
|
+
kind: update
|
|
181
|
+
risk: workspace-write
|
|
182
|
+
idempotent: true
|
|
183
|
+
description: Activate a composed optional module for this workspace after its dependencies are active.
|
|
184
|
+
- id: deactivate-module
|
|
185
|
+
entity: module-activation
|
|
186
|
+
permission: system.settings.manage
|
|
187
|
+
kind: update
|
|
188
|
+
risk: workspace-write
|
|
189
|
+
idempotent: true
|
|
190
|
+
description: Deactivate a composed optional module for this workspace unless a required module or an active dependent needs it.
|
|
126
191
|
- id: set-flag
|
|
127
192
|
permission: system.settings.manage
|
|
128
193
|
kind: update
|
|
129
194
|
risk: workspace-write
|
|
130
195
|
idempotent: true
|
|
131
196
|
description: Turn a declared feature flag on or off for this workspace, or clear the override so the module default applies again.
|
|
197
|
+
decisions:
|
|
198
|
+
- id: D-MODULE-ACTIVATION-SCOPE
|
|
199
|
+
question: What can the administration UI change about modules?
|
|
200
|
+
answer: Per-workspace activation of the modules the application already composes; adding or removing a module stays a CLI operation because the composition is baked at build.
|
|
201
|
+
decidedBy: user
|
|
202
|
+
- id: D-MODULE-ACTIVATION-REQUIRED
|
|
203
|
+
question: Which modules can never be deactivated?
|
|
204
|
+
answer: system.core, auth.core, users.core and profile.core, and any module another active module depends on through a declared module dependency or a required public capability; the refusal names the active dependents.
|
|
205
|
+
decidedBy: user
|
|
206
|
+
- id: D-MODULE-ACTIVATION-DEFAULT
|
|
207
|
+
question: What is the state of a module nobody has touched?
|
|
208
|
+
answer: Active; the table holds only decisions an owner made, and a row is written for the module being changed, never for the whole set.
|
|
209
|
+
decidedBy: default
|
|
210
|
+
- id: D-MODULE-ACTIVATION-SNAPSHOT
|
|
211
|
+
question: How often is the activation read?
|
|
212
|
+
answer: Once per tenant into a memoised snapshot held for thirty seconds and at most 1024 tenants per process, invalidated at once by a change through the same instance; another instance sees the change within the snapshot lifetime.
|
|
213
|
+
decidedBy: default
|
|
214
|
+
- id: D-MODULE-ACTIVATION-AUDIT
|
|
215
|
+
question: Where is a change recorded?
|
|
216
|
+
answer: One system.module.activated or system.module.deactivated event in the auth.core audit trail, carrying the module id, the resulting state and the actor; a no-op repeat writes nothing.
|
|
217
|
+
decidedBy: default
|
|
218
|
+
- id: D-MODULE-ACTIVATION-SHELL
|
|
219
|
+
question: How does the shell learn which modules are active?
|
|
220
|
+
answer: From GET /api/system/modules/active, served to every member holding system.workspace.access, read once per workspace before the shell renders; a failed read shows every module because the server gate still refuses an inactive module's API.
|
|
221
|
+
decidedBy: default
|
|
222
|
+
- id: D-MODULE-ACTIVATION-AGENT-TOOLS
|
|
223
|
+
question: Are the agent tools of an inactive module withheld?
|
|
224
|
+
answer: Not yet. The harness tool registry has no per-tenant module hook and a tool carries no module id, so this is a documented follow-up; a tool of an inactive module that calls the module's service still runs.
|
|
225
|
+
decidedBy: default
|
|
226
|
+
outOfScope:
|
|
227
|
+
- Adding or removing a module from a running application through the UI; the composition is CLI-owned and baked at build.
|
|
228
|
+
- Withholding the agent tools of an inactive module from a workspace, which needs a per-tenant hook in the harness tool registry.
|
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo } from 'octane';
|
|
|
2
2
|
import {
|
|
3
3
|
Alert,
|
|
4
4
|
Button,
|
|
5
|
+
ConfirmDialog,
|
|
5
6
|
Drawer,
|
|
6
7
|
EmptyState,
|
|
7
8
|
Icon,
|
|
@@ -14,11 +15,21 @@ import {
|
|
|
14
15
|
} from '@flowdular/sdk/ui';
|
|
15
16
|
import { activeLocale, t } from '@flowdular/sdk/client';
|
|
16
17
|
import { useValue } from 'segment-state';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ApiError,
|
|
20
|
+
loadModuleCatalog,
|
|
21
|
+
setModuleActivation,
|
|
22
|
+
type ModuleCatalogEntry,
|
|
23
|
+
} from './api.ts';
|
|
18
24
|
import { FlagsPanel } from './FlagsPanel.tsrx';
|
|
19
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
activationLabel,
|
|
27
|
+
activationTone,
|
|
28
|
+
moduleColumns,
|
|
29
|
+
surfaces,
|
|
30
|
+
} from './module-columns.tsrx';
|
|
20
31
|
import { ModuleSettingsSection } from './ModuleSettingsSection.tsrx';
|
|
21
|
-
import { createModulesClientState } from './state.ts';
|
|
32
|
+
import { activationRefusal, createModulesClientState } from './state.ts';
|
|
22
33
|
|
|
23
34
|
const TABS_ID = 'system-modules';
|
|
24
35
|
|
|
@@ -38,6 +49,11 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
38
49
|
client.state.selectedModuleId,
|
|
39
50
|
);
|
|
40
51
|
const [tab, setTab] = useValue(client.state.tab);
|
|
52
|
+
const [pending, setPending] = useValue(client.state.pending);
|
|
53
|
+
const [busyModuleId, setBusyModuleId] = useValue(client.state.busyModuleId);
|
|
54
|
+
const [activationError, setActivationError] = useValue(
|
|
55
|
+
client.state.activationError,
|
|
56
|
+
);
|
|
41
57
|
const selected =
|
|
42
58
|
modules.find((module) => module.id === selectedModuleId) ?? null;
|
|
43
59
|
const term = query.trim().toLocaleLowerCase(activeLocale());
|
|
@@ -50,6 +66,9 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
50
66
|
module.name.toLocaleLowerCase(activeLocale()).includes(term),
|
|
51
67
|
);
|
|
52
68
|
const enabled = modules.filter((module) => module.enabled);
|
|
69
|
+
const active = enabled.filter((module) => module.active);
|
|
70
|
+
const pendingModule =
|
|
71
|
+
modules.find((module) => module.id === pending?.moduleId) ?? null;
|
|
53
72
|
|
|
54
73
|
const refresh = async () => {
|
|
55
74
|
setStatus('loading');
|
|
@@ -76,6 +95,33 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
76
95
|
void refresh();
|
|
77
96
|
}, []);
|
|
78
97
|
|
|
98
|
+
/* The server is the authority on dependencies: a refusal it answers is
|
|
99
|
+
shown with the modules it named, and the catalog is read again so the
|
|
100
|
+
table never shows a state the workspace no longer has. */
|
|
101
|
+
const confirmActivation = async () => {
|
|
102
|
+
if (!pending) return;
|
|
103
|
+
setBusyModuleId(pending.moduleId);
|
|
104
|
+
setActivationError('');
|
|
105
|
+
try {
|
|
106
|
+
await setModuleActivation(
|
|
107
|
+
pending.moduleId,
|
|
108
|
+
pending.active,
|
|
109
|
+
props.csrfToken,
|
|
110
|
+
);
|
|
111
|
+
setPending(null);
|
|
112
|
+
await refresh();
|
|
113
|
+
} catch (changeError) {
|
|
114
|
+
setPending(null);
|
|
115
|
+
setActivationError(
|
|
116
|
+
changeError instanceof Error
|
|
117
|
+
? changeError.message
|
|
118
|
+
: t('system.error.activation'),
|
|
119
|
+
);
|
|
120
|
+
} finally {
|
|
121
|
+
setBusyModuleId(null);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
79
125
|
<div class="ui-view">
|
|
80
126
|
<PageHeader
|
|
81
127
|
eyebrow={t('system.page.eyebrow')}
|
|
@@ -118,9 +164,13 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
118
164
|
@if (status === 'error') {
|
|
119
165
|
<Alert>{error}</Alert>
|
|
120
166
|
}
|
|
167
|
+
@if (activationError !== '') {
|
|
168
|
+
<Alert>{activationError}</Alert>
|
|
169
|
+
}
|
|
121
170
|
<TableCard
|
|
122
171
|
title={t('system.table.title')}
|
|
123
172
|
count={t('system.table.count', {
|
|
173
|
+
active: active.length,
|
|
124
174
|
enabled: enabled.length,
|
|
125
175
|
total: modules.length,
|
|
126
176
|
})}
|
|
@@ -158,6 +208,23 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
158
208
|
icon: 'external',
|
|
159
209
|
onSelect: () => setSelectedModuleId(module.id),
|
|
160
210
|
},
|
|
211
|
+
{
|
|
212
|
+
id: 'activation',
|
|
213
|
+
label: module.enabled && module.active
|
|
214
|
+
? t('system.action.deactivate')
|
|
215
|
+
: t('system.action.activate'),
|
|
216
|
+
icon: module.enabled && module.active ? 'x' : 'play',
|
|
217
|
+
tone: module.enabled && module.active ? 'danger' : 'default',
|
|
218
|
+
disabled: busyModuleId !== null ||
|
|
219
|
+
activationRefusal(module, props.canManageSettings, t) !==
|
|
220
|
+
null,
|
|
221
|
+
reason: activationRefusal(module, props.canManageSettings, t) ??
|
|
222
|
+
undefined,
|
|
223
|
+
onSelect: () => setPending({
|
|
224
|
+
moduleId: module.id,
|
|
225
|
+
active: !module.active,
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
161
228
|
]}
|
|
162
229
|
noteIcon="code"
|
|
163
230
|
note={<>
|
|
@@ -173,6 +240,28 @@ export function ModulesView(props: ModulesViewProps) @{
|
|
|
173
240
|
</>
|
|
174
241
|
}
|
|
175
242
|
</div>
|
|
243
|
+
<ConfirmDialog
|
|
244
|
+
open={pending !== null}
|
|
245
|
+
title={pending?.active
|
|
246
|
+
? t('system.activation.confirmActivateTitle', {
|
|
247
|
+
module: pendingModule?.name ?? pending.moduleId,
|
|
248
|
+
})
|
|
249
|
+
: t('system.activation.confirmDeactivateTitle', {
|
|
250
|
+
module: pendingModule?.name ?? pending?.moduleId ?? '',
|
|
251
|
+
})}
|
|
252
|
+
confirmLabel={pending?.active
|
|
253
|
+
? t('system.action.activate')
|
|
254
|
+
: t('system.action.deactivate')}
|
|
255
|
+
cancelLabel={t('system.action.cancel')}
|
|
256
|
+
tone={pending?.active ? 'primary' : 'danger'}
|
|
257
|
+
busy={busyModuleId !== null}
|
|
258
|
+
onConfirm={() => void confirmActivation()}
|
|
259
|
+
onCancel={() => setPending(null)}
|
|
260
|
+
>
|
|
261
|
+
{pending?.active
|
|
262
|
+
? t('system.activation.confirmActivate')
|
|
263
|
+
: t('system.activation.confirmDeactivate')}
|
|
264
|
+
</ConfirmDialog>
|
|
176
265
|
<Drawer
|
|
177
266
|
open={tab === 'catalog' && selected !== null}
|
|
178
267
|
title={selected?.name ?? t('system.drawer.module')}
|
|
@@ -224,11 +313,9 @@ function ModuleDetail(props: {
|
|
|
224
313
|
</span>
|
|
225
314
|
<span class="ui-cell">
|
|
226
315
|
<small>{t('system.table.status')}</small>
|
|
227
|
-
<Tag tone={props.module
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
: t('system.status.available')}
|
|
231
|
-
</Tag>
|
|
316
|
+
<Tag tone={activationTone(props.module)} dot>{activationLabel(
|
|
317
|
+
props.module,
|
|
318
|
+
)}</Tag>
|
|
232
319
|
</span>
|
|
233
320
|
</div>
|
|
234
321
|
<span class="ui-cell">
|
|
@@ -241,6 +328,12 @@ function ModuleDetail(props: {
|
|
|
241
328
|
<small>{t('system.detail.directory')}</small>
|
|
242
329
|
<span class="ui-mono">{props.module.directory}</span>
|
|
243
330
|
</span>
|
|
331
|
+
<span class="ui-cell">
|
|
332
|
+
<small>{t('system.table.dependents')}</small>
|
|
333
|
+
<span class="ui-mono">
|
|
334
|
+
{props.module.dependents.join(', ') || t('system.value.none')}
|
|
335
|
+
</span>
|
|
336
|
+
</span>
|
|
244
337
|
</div>
|
|
245
338
|
<div class="ui-form__section">
|
|
246
339
|
<div class="ui-form__section-head">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModuleCatalogPayload } from '../server/endpoints.ts';
|
|
2
|
+
import type { ModuleActivationEntry } from '../domain/modules.ts';
|
|
2
3
|
import { t } from '@flowdular/sdk/client/i18n';
|
|
3
4
|
|
|
4
|
-
export type
|
|
5
|
+
export type ModuleCatalogEntry = ModuleCatalogPayload;
|
|
5
6
|
|
|
6
7
|
export interface ModuleCatalog {
|
|
7
8
|
readonly modules: readonly ModuleCatalogEntry[];
|
|
@@ -10,27 +11,67 @@ export interface ModuleCatalog {
|
|
|
10
11
|
|
|
11
12
|
export class ApiError extends Error {
|
|
12
13
|
readonly status: number;
|
|
14
|
+
/** The modules a refusal names, when the server named any. */
|
|
15
|
+
readonly modules: readonly string[];
|
|
13
16
|
|
|
14
|
-
constructor(
|
|
17
|
+
constructor(
|
|
18
|
+
status: number,
|
|
19
|
+
message: string,
|
|
20
|
+
modules: readonly string[] = [],
|
|
21
|
+
) {
|
|
15
22
|
super(message);
|
|
16
23
|
this.name = 'ApiError';
|
|
17
24
|
this.status = status;
|
|
25
|
+
this.modules = modules;
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
readonly error?: { readonly message?: string };
|
|
29
|
+
async function payload<T>(response: Response, fallback: string): Promise<T> {
|
|
30
|
+
const value = (await response.json()) as T & {
|
|
31
|
+
readonly error?: {
|
|
32
|
+
readonly message?: string;
|
|
33
|
+
readonly modules?: readonly string[];
|
|
34
|
+
};
|
|
28
35
|
};
|
|
29
36
|
if (!response.ok) {
|
|
30
37
|
throw new ApiError(
|
|
31
38
|
response.status,
|
|
32
|
-
value.error?.message ??
|
|
39
|
+
value.error?.message ?? fallback,
|
|
40
|
+
value.error?.modules ?? [],
|
|
33
41
|
);
|
|
34
42
|
}
|
|
35
43
|
return value;
|
|
36
44
|
}
|
|
45
|
+
|
|
46
|
+
export async function loadModuleCatalog(): Promise<ModuleCatalog> {
|
|
47
|
+
const response = await fetch('/api/system/modules', {
|
|
48
|
+
headers: { accept: 'application/json' },
|
|
49
|
+
credentials: 'same-origin',
|
|
50
|
+
});
|
|
51
|
+
return payload<ModuleCatalog>(response, t('system.error.catalog'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function setModuleActivation(
|
|
55
|
+
moduleId: string,
|
|
56
|
+
active: boolean,
|
|
57
|
+
csrfToken: string,
|
|
58
|
+
): Promise<ModuleActivationEntry> {
|
|
59
|
+
const response = await fetch(
|
|
60
|
+
active ? '/api/system/modules/activate' : '/api/system/modules/deactivate',
|
|
61
|
+
{
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': 'application/json',
|
|
65
|
+
'x-csrf-token': csrfToken,
|
|
66
|
+
},
|
|
67
|
+
credentials: 'same-origin',
|
|
68
|
+
body: JSON.stringify({ moduleId }),
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
return (
|
|
72
|
+
await payload<{ readonly module: ModuleActivationEntry }>(
|
|
73
|
+
response,
|
|
74
|
+
t('system.error.activation'),
|
|
75
|
+
)
|
|
76
|
+
).module;
|
|
77
|
+
}
|