@antelopejs/dms 0.9.0 → 0.9.1
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/CHANGELOG.md +16 -0
- package/data/module-store/catalog.json +71 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.d.ts +2 -0
- package/dist/implementations/dms/frontend-modules.js +29 -1
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/frontend-ready.d.ts +8 -0
- package/dist/implementations/dms/frontend-ready.js +34 -0
- package/dist/implementations/dms/frontend-ready.js.map +1 -0
- package/dist/implementations/dms/manifest.d.ts +1 -0
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +2 -0
- package/dist/implementations/dms/page.js +16 -0
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/store.d.ts +21 -0
- package/dist/routes/store.js +130 -0
- package/dist/routes/store.js.map +1 -0
- package/dist/store/activity.d.ts +2 -0
- package/dist/store/activity.js +12 -0
- package/dist/store/activity.js.map +1 -0
- package/dist/store/cache.d.ts +8 -0
- package/dist/store/cache.js +29 -0
- package/dist/store/cache.js.map +1 -0
- package/dist/store/catalog.d.ts +4 -0
- package/dist/store/catalog.js +69 -0
- package/dist/store/catalog.js.map +1 -0
- package/dist/store/compatibility.d.ts +3 -0
- package/dist/store/compatibility.js +45 -0
- package/dist/store/compatibility.js.map +1 -0
- package/dist/store/declarations.d.ts +18 -0
- package/dist/store/declarations.js +116 -0
- package/dist/store/declarations.js.map +1 -0
- package/dist/store/frontend-compatibility.d.ts +18 -0
- package/dist/store/frontend-compatibility.js +93 -0
- package/dist/store/frontend-compatibility.js.map +1 -0
- package/dist/store/interfaces.d.ts +6 -0
- package/dist/store/interfaces.js +126 -0
- package/dist/store/interfaces.js.map +1 -0
- package/dist/store/listing.d.ts +2 -0
- package/dist/store/listing.js +180 -0
- package/dist/store/listing.js.map +1 -0
- package/dist/store/operations.d.ts +8 -0
- package/dist/store/operations.js +254 -0
- package/dist/store/operations.js.map +1 -0
- package/dist/store/registry.d.ts +4 -0
- package/dist/store/registry.js +61 -0
- package/dist/store/registry.js.map +1 -0
- package/dist/store/requirements.d.ts +14 -0
- package/dist/store/requirements.js +81 -0
- package/dist/store/requirements.js.map +1 -0
- package/dist/store/runtime.d.ts +11 -0
- package/dist/store/runtime.js +78 -0
- package/dist/store/runtime.js.map +1 -0
- package/dist/store/tarball.d.ts +2 -0
- package/dist/store/tarball.js +80 -0
- package/dist/store/tarball.js.map +1 -0
- package/dist/store/types.d.ts +98 -0
- package/dist/store/types.js +3 -0
- package/dist/store/types.js.map +1 -0
- package/dist/test/unit/store/store.test.d.ts +1 -0
- package/dist/test/unit/store/store.test.js +127 -0
- package/dist/test/unit/store/store.test.js.map +1 -0
- package/dist/validation/store.schema.d.ts +39 -0
- package/dist/validation/store.schema.js +27 -0
- package/dist/validation/store.schema.js.map +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
- package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
- package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +335 -34
- package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
- package/frontend-vue/package.json +3 -3
- package/frontend-vue/pnpm-lock.yaml +16 -22
- package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
- package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
- package/package.json +1 -1
|
@@ -1,137 +1,203 @@
|
|
|
1
|
+
import type { Tone } from "#dms-ui/app/types/tone";
|
|
2
|
+
import type { StatusPillDot } from "#dms-ui/app/components/status-pill/StatusPill.vue";
|
|
3
|
+
import type {
|
|
4
|
+
StoreListingEntry,
|
|
5
|
+
StoreModuleState,
|
|
6
|
+
StoreOperationKind,
|
|
7
|
+
} from "../../types/module-store";
|
|
1
8
|
import type { DisplayResolver } from "./modules-catalog";
|
|
2
9
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
/** A store module as the Modules page filters and draws it. */
|
|
11
|
+
export type ModuleStoreEntry = StoreListingEntry & {
|
|
12
|
+
/** Store category, a `$i18n.key` (what the category chips read). */
|
|
13
|
+
catalogCategory: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Which store modules the store section shows. */
|
|
17
|
+
export type StoreScope = "all" | "available" | "installed";
|
|
18
|
+
|
|
19
|
+
/** What a store tile's pill says. */
|
|
20
|
+
export interface StoreTileStatus {
|
|
21
|
+
tone: Tone;
|
|
22
|
+
/** i18n key under `modules.store.state`. */
|
|
23
|
+
labelKey: string;
|
|
24
|
+
dot?: StatusPillDot;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One action a store tile offers. */
|
|
28
|
+
export interface StoreAction {
|
|
29
|
+
kind: StoreOperationKind;
|
|
30
|
+
/** i18n key under `modules.store.actions`. */
|
|
31
|
+
labelKey: string;
|
|
32
|
+
icon: string;
|
|
33
|
+
/** The action runs only once the owner confirms it. */
|
|
34
|
+
confirm?: boolean;
|
|
9
35
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
36
|
+
* Offered in the tile's menu only, never as its button: pausing or
|
|
37
|
+
* removing a running module is not what its tile should invite.
|
|
12
38
|
*/
|
|
13
|
-
|
|
14
|
-
/** npm package the AntelopeJS CLI adds to the project. */
|
|
15
|
-
packageName: string;
|
|
16
|
-
/** Display name, a `$i18n.key`. */
|
|
17
|
-
title: string;
|
|
18
|
-
/** One-line summary, a `$i18n.key`. */
|
|
19
|
-
description: string;
|
|
20
|
-
/** Iconify name, the icon the module registers once installed. */
|
|
21
|
-
icon: string;
|
|
22
|
-
/** Store category, a `$i18n.key`. */
|
|
23
|
-
catalogCategory: string;
|
|
39
|
+
isMenuOnly?: boolean;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const CATEGORY_PREFIX = "$modules.store.category";
|
|
43
|
+
|
|
44
|
+
/** The command that turns a production frontend into one serving new layers. */
|
|
45
|
+
export const FRONTEND_BUILD_COMMAND = "ajs dms build";
|
|
46
|
+
|
|
47
|
+
const STATUSES: Record<StoreModuleState, StoreTileStatus> = {
|
|
48
|
+
available: { tone: "neutral", labelKey: "available", dot: "none" },
|
|
49
|
+
busy: { tone: "info", labelKey: "busy", dot: "live" },
|
|
50
|
+
active: { tone: "success", labelKey: "active" },
|
|
51
|
+
disabled: { tone: "neutral", labelKey: "disabled" },
|
|
52
|
+
error: { tone: "error", labelKey: "error" },
|
|
53
|
+
pending: { tone: "warning", labelKey: "pending" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ACTIONS: Record<StoreOperationKind, StoreAction> = {
|
|
57
|
+
install: {
|
|
58
|
+
kind: "install",
|
|
59
|
+
labelKey: "install",
|
|
60
|
+
icon: "i-ph-download-simple",
|
|
61
|
+
},
|
|
62
|
+
update: { kind: "update", labelKey: "update", icon: "i-ph-arrow-circle-up" },
|
|
63
|
+
enable: { kind: "enable", labelKey: "enable", icon: "i-ph-play" },
|
|
64
|
+
disable: {
|
|
65
|
+
kind: "disable",
|
|
66
|
+
labelKey: "disable",
|
|
67
|
+
icon: "i-ph-pause",
|
|
68
|
+
isMenuOnly: true,
|
|
69
|
+
},
|
|
70
|
+
uninstall: {
|
|
71
|
+
kind: "uninstall",
|
|
72
|
+
labelKey: "uninstall",
|
|
73
|
+
icon: "i-ph-trash",
|
|
74
|
+
confirm: true,
|
|
75
|
+
isMenuOnly: true,
|
|
76
|
+
},
|
|
77
|
+
apply: { kind: "apply", labelKey: "apply", icon: "i-ph-arrows-clockwise" },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** States of a module that antelope.config.ts declares. */
|
|
81
|
+
const INSTALLED_STATES = new Set<StoreModuleState>([
|
|
82
|
+
"active",
|
|
83
|
+
"disabled",
|
|
84
|
+
"pending",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
export function toModuleStoreEntry(entry: StoreListingEntry): ModuleStoreEntry {
|
|
42
88
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
89
|
+
...entry,
|
|
90
|
+
catalogCategory: `${CATEGORY_PREFIX}.${entry.category}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether the module is installed, from the store or by the config. */
|
|
95
|
+
export function isInstalled(entry: StoreListingEntry): boolean {
|
|
96
|
+
if (INSTALLED_STATES.has(entry.state)) return true;
|
|
97
|
+
return entry.installedVersion !== undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function filterByScope<T extends StoreListingEntry>(
|
|
101
|
+
entries: readonly T[],
|
|
102
|
+
scope: StoreScope,
|
|
103
|
+
): T[] {
|
|
104
|
+
if (scope === "all") return [...entries];
|
|
105
|
+
const wantsInstalled = scope === "installed";
|
|
106
|
+
return entries.filter((entry) => isInstalled(entry) === wantsInstalled);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The store modules, alphabetical by their shown name. */
|
|
110
|
+
export function sortStoreModules<T extends StoreListingEntry>(
|
|
111
|
+
entries: readonly T[],
|
|
112
|
+
resolve: DisplayResolver,
|
|
113
|
+
): T[] {
|
|
114
|
+
return [...entries].sort((a, b) =>
|
|
115
|
+
resolve(a.title).localeCompare(resolve(b.title)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function storeTileStatus(entry: StoreListingEntry): StoreTileStatus {
|
|
120
|
+
if (entry.state === "active" && entry.updateAvailable) {
|
|
121
|
+
return { tone: "warning", labelKey: "update_available" };
|
|
122
|
+
}
|
|
123
|
+
return STATUSES[entry.state];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What the tile offers, the main action first. A module in error offers to
|
|
128
|
+
* try its last step again: installing it when the file does not hold it,
|
|
129
|
+
* applying the file otherwise.
|
|
130
|
+
*/
|
|
131
|
+
export function storeActions(entry: StoreListingEntry): StoreAction[] {
|
|
132
|
+
const installed = entry.installedVersion !== undefined;
|
|
133
|
+
const byState: Record<StoreModuleState, StoreOperationKind[]> = {
|
|
134
|
+
available: ["install"],
|
|
135
|
+
busy: [],
|
|
136
|
+
active: entry.updateAvailable
|
|
137
|
+
? ["update", "disable", "uninstall"]
|
|
138
|
+
: ["disable", "uninstall"],
|
|
139
|
+
disabled: ["enable", "uninstall"],
|
|
140
|
+
error: installed ? ["apply", "uninstall"] : ["install"],
|
|
141
|
+
pending: ["apply", "uninstall"],
|
|
49
142
|
};
|
|
143
|
+
return byState[entry.state].map((kind) => ACTIONS[kind]);
|
|
50
144
|
}
|
|
51
145
|
|
|
52
146
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Media and Builder register no catalog module, so theirs come from their
|
|
56
|
-
* settings page and the v2 design.
|
|
147
|
+
* Whether the page waits on the frontend for this module: its backend side
|
|
148
|
+
* changed and the frontend server has not reported serving it yet.
|
|
57
149
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
storeEntry(
|
|
61
|
-
"api",
|
|
62
|
-
"@antelopejs/dms-api",
|
|
63
|
-
"i-ph-brackets-curly",
|
|
64
|
-
CATEGORY.developer,
|
|
65
|
-
),
|
|
66
|
-
storeEntry(
|
|
67
|
-
"automation",
|
|
68
|
-
"@antelopejs/dms-automation",
|
|
69
|
-
"i-ph-flow-arrow",
|
|
70
|
-
CATEGORY.operations,
|
|
71
|
-
),
|
|
72
|
-
storeEntry(
|
|
73
|
-
"builder",
|
|
74
|
-
"@antelopejs/dms-builder",
|
|
75
|
-
"i-ph-hammer",
|
|
76
|
-
CATEGORY.developer,
|
|
77
|
-
),
|
|
78
|
-
storeEntry(
|
|
79
|
-
"cicd",
|
|
80
|
-
"@antelopejs-private/dms-cicd",
|
|
81
|
-
"i-ph-rocket-launch",
|
|
82
|
-
CATEGORY.operations,
|
|
83
|
-
),
|
|
84
|
-
storeEntry(
|
|
85
|
-
"database",
|
|
86
|
-
"@antelopejs/dms-database",
|
|
87
|
-
"i-ph-database",
|
|
88
|
-
CATEGORY.data,
|
|
89
|
-
),
|
|
90
|
-
storeEntry(
|
|
91
|
-
"lang",
|
|
92
|
-
"@antelopejs/dms-lang",
|
|
93
|
-
"i-ph-translate",
|
|
94
|
-
CATEGORY.content,
|
|
95
|
-
),
|
|
96
|
-
storeEntry(
|
|
97
|
-
"mailing",
|
|
98
|
-
"@antelopejs/dms-mailing",
|
|
99
|
-
"i-ph-envelope-simple",
|
|
100
|
-
CATEGORY.growth,
|
|
101
|
-
),
|
|
102
|
-
storeEntry(
|
|
103
|
-
"marketing",
|
|
104
|
-
"@antelopejs/dms-marketing",
|
|
105
|
-
"i-ph-chart-line-up",
|
|
106
|
-
CATEGORY.growth,
|
|
107
|
-
),
|
|
108
|
-
storeEntry("media", "@antelopejs/dms-media", "i-ph-images", CATEGORY.content),
|
|
109
|
-
storeEntry(
|
|
110
|
-
"saas",
|
|
111
|
-
"@antelopejs/dms-saas",
|
|
112
|
-
"i-ph-buildings",
|
|
113
|
-
CATEGORY.business,
|
|
114
|
-
),
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
/** The AntelopeJS CLI command that adds a package module to a project. */
|
|
118
|
-
export const MODULE_INSTALL_COMMAND = "ajs project modules add";
|
|
119
|
-
|
|
120
|
-
/** The full CLI command that adds the module to a project. */
|
|
121
|
-
export function moduleInstallCommand(entry: ModuleStoreEntry): string {
|
|
122
|
-
return `${MODULE_INSTALL_COMMAND} ${entry.packageName}`;
|
|
150
|
+
export function isAwaitingFrontend(entry: StoreListingEntry): boolean {
|
|
151
|
+
return entry.state !== "busy" && entry.frontend === "pending";
|
|
123
152
|
}
|
|
124
153
|
|
|
154
|
+
/** Why an action the tile offers cannot run, as the tile explains it. */
|
|
155
|
+
export type StoreActionBlock =
|
|
156
|
+
| "no_release"
|
|
157
|
+
| "incompatible"
|
|
158
|
+
| "frontend"
|
|
159
|
+
| "interfaces"
|
|
160
|
+
| "dependents";
|
|
161
|
+
|
|
162
|
+
/** Actions that load a new release of the module. */
|
|
163
|
+
const RELEASE_ACTIONS = new Set<StoreOperationKind>(["install", "update"]);
|
|
125
164
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
165
|
+
* Actions that pick the missing interfaces' modules in a dialog; the others
|
|
166
|
+
* refuse a release that misses one.
|
|
128
167
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
const PICKS_DEPENDENCIES = new Set<StoreOperationKind>(["install"]);
|
|
169
|
+
/** Actions that take a running module down. */
|
|
170
|
+
const TAKE_DOWN_ACTIONS = new Set<StoreOperationKind>(["disable", "uninstall"]);
|
|
171
|
+
|
|
172
|
+
function releaseBlock(
|
|
173
|
+
entry: StoreListingEntry,
|
|
174
|
+
kind: StoreOperationKind,
|
|
175
|
+
): StoreActionBlock | undefined {
|
|
176
|
+
if (!entry.latestVersion) return "no_release";
|
|
177
|
+
if (!entry.compatible) return "incompatible";
|
|
178
|
+
if (entry.frontendRequirement) return "frontend";
|
|
179
|
+
if (!PICKS_DEPENDENCIES.has(kind) && entry.missingInterfaces.length > 0) {
|
|
180
|
+
return "interfaces";
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Why `kind` cannot run on the module, or undefined when it can. */
|
|
186
|
+
export function storeActionBlock(
|
|
187
|
+
entry: StoreListingEntry,
|
|
188
|
+
kind: StoreOperationKind,
|
|
189
|
+
): StoreActionBlock | undefined {
|
|
190
|
+
if (RELEASE_ACTIONS.has(kind)) return releaseBlock(entry, kind);
|
|
191
|
+
if (TAKE_DOWN_ACTIONS.has(kind) && entry.dependents.length > 0) {
|
|
192
|
+
return "dependents";
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Whether `kind` first asks which modules implement the missing interfaces. */
|
|
198
|
+
export function needsDependencyChoice(
|
|
199
|
+
entry: StoreListingEntry,
|
|
200
|
+
kind: StoreOperationKind,
|
|
201
|
+
): boolean {
|
|
202
|
+
return PICKS_DEPENDENCIES.has(kind) && entry.missingInterfaces.length > 0;
|
|
137
203
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MissingInterface,
|
|
3
|
+
StoreActionBody,
|
|
4
|
+
StoreDependencyChoice,
|
|
5
|
+
} from "../../types/module-store";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The radio value of the "Other npm package" choice: not an npm package
|
|
9
|
+
* name, so no candidate can take it.
|
|
10
|
+
*/
|
|
11
|
+
export const OTHER_PACKAGE_CHOICE = ":other";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An npm package name, scoped or not: never a path, a URL or a git source,
|
|
15
|
+
* as the store only installs from the registry.
|
|
16
|
+
*/
|
|
17
|
+
const NPM_PACKAGE_PATTERN =
|
|
18
|
+
/^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
19
|
+
const NPM_PACKAGE_MAX_LENGTH = 214;
|
|
20
|
+
|
|
21
|
+
/** What the install dialog holds for one missing interface. */
|
|
22
|
+
export interface DependencyDraft {
|
|
23
|
+
interfacePackage: string;
|
|
24
|
+
/** A candidate's package, or {@link OTHER_PACKAGE_CHOICE}. */
|
|
25
|
+
choice: string;
|
|
26
|
+
/** The package typed for the "Other npm package" choice. */
|
|
27
|
+
otherPackage: string;
|
|
28
|
+
/** The config as typed, JSON; empty means none. */
|
|
29
|
+
config: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Why a draft cannot be sent, by field: i18n keys under the dialog's errors. */
|
|
33
|
+
export type DependencyDraftError =
|
|
34
|
+
| "package_required"
|
|
35
|
+
| "package_invalid"
|
|
36
|
+
| "config_invalid";
|
|
37
|
+
|
|
38
|
+
export interface DependencyDraftErrors {
|
|
39
|
+
package?: DependencyDraftError;
|
|
40
|
+
config?: DependencyDraftError;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A config as the dialog reads it: absent when left empty. */
|
|
44
|
+
export interface ParsedDependencyConfig {
|
|
45
|
+
isValid: boolean;
|
|
46
|
+
config?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isNpmPackageName(value: string): boolean {
|
|
50
|
+
return (
|
|
51
|
+
value.length <= NPM_PACKAGE_MAX_LENGTH && NPM_PACKAGE_PATTERN.test(value)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
56
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
|
|
58
|
+
/** Reads a typed config: empty is no config, anything else a JSON object. */
|
|
59
|
+
export function parseDependencyConfig(text: string): ParsedDependencyConfig {
|
|
60
|
+
if (!text.trim()) return { isValid: true };
|
|
61
|
+
try {
|
|
62
|
+
const value: unknown = JSON.parse(text);
|
|
63
|
+
return isPlainObject(value)
|
|
64
|
+
? { isValid: true, config: value }
|
|
65
|
+
: { isValid: false };
|
|
66
|
+
} catch {
|
|
67
|
+
return { isValid: false };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One draft per missing interface, the first candidate picked. A draft of
|
|
73
|
+
* an earlier attempt at the same interface is kept, so a retry starts from
|
|
74
|
+
* what the owner typed.
|
|
75
|
+
*/
|
|
76
|
+
export function createDependencyDrafts(
|
|
77
|
+
missing: readonly MissingInterface[],
|
|
78
|
+
previous: readonly DependencyDraft[] = [],
|
|
79
|
+
): DependencyDraft[] {
|
|
80
|
+
return missing.map(({ interfacePackage, candidates }) => {
|
|
81
|
+
const earlier = previous.find(
|
|
82
|
+
(draft) => draft.interfacePackage === interfacePackage,
|
|
83
|
+
);
|
|
84
|
+
if (earlier) return { ...earlier };
|
|
85
|
+
return {
|
|
86
|
+
interfacePackage,
|
|
87
|
+
choice: candidates[0]?.package ?? OTHER_PACKAGE_CHOICE,
|
|
88
|
+
otherPackage: "",
|
|
89
|
+
config: "",
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The package a draft installs, as the owner picked or typed it. */
|
|
95
|
+
export function draftPackage(draft: DependencyDraft): string {
|
|
96
|
+
return draft.choice === OTHER_PACKAGE_CHOICE
|
|
97
|
+
? draft.otherPackage.trim()
|
|
98
|
+
: draft.choice;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function packageError(
|
|
102
|
+
draft: DependencyDraft,
|
|
103
|
+
): DependencyDraftError | undefined {
|
|
104
|
+
const packageName = draftPackage(draft);
|
|
105
|
+
if (!packageName) return "package_required";
|
|
106
|
+
return isNpmPackageName(packageName) ? undefined : "package_invalid";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validateDependencyDraft(
|
|
110
|
+
draft: DependencyDraft,
|
|
111
|
+
): DependencyDraftErrors {
|
|
112
|
+
const errors: DependencyDraftErrors = {};
|
|
113
|
+
const packageProblem = packageError(draft);
|
|
114
|
+
if (packageProblem) errors.package = packageProblem;
|
|
115
|
+
if (!parseDependencyConfig(draft.config).isValid) {
|
|
116
|
+
errors.config = "config_invalid";
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function areDependencyDraftsValid(
|
|
122
|
+
drafts: readonly DependencyDraft[],
|
|
123
|
+
): boolean {
|
|
124
|
+
return drafts.every(
|
|
125
|
+
(draft) => Object.keys(validateDependencyDraft(draft)).length === 0,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toDependencyChoice(draft: DependencyDraft): StoreDependencyChoice {
|
|
130
|
+
const { config } = parseDependencyConfig(draft.config);
|
|
131
|
+
const choice: StoreDependencyChoice = {
|
|
132
|
+
interfacePackage: draft.interfacePackage,
|
|
133
|
+
package: draftPackage(draft),
|
|
134
|
+
};
|
|
135
|
+
if (config) choice.config = config;
|
|
136
|
+
return choice;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The install request for module `id`, from drafts already validated. */
|
|
140
|
+
export function buildInstallBody(
|
|
141
|
+
id: string,
|
|
142
|
+
drafts: readonly DependencyDraft[],
|
|
143
|
+
): StoreActionBody {
|
|
144
|
+
if (drafts.length === 0) return { id };
|
|
145
|
+
return { id, dependencies: drafts.map(toDependencyChoice) };
|
|
146
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StoreActionBody,
|
|
3
|
+
StoreListing,
|
|
4
|
+
StoreListingEntry,
|
|
5
|
+
StoreOperationKind,
|
|
6
|
+
} from "../../types/module-store";
|
|
7
|
+
import { isAwaitingFrontend } from "../../build/utils/module-store-catalog";
|
|
8
|
+
|
|
9
|
+
export const ENDPOINT_STORE_LISTING = "/dms/store/listing";
|
|
10
|
+
const ENDPOINT_STORE_ACTION = "/dms/store";
|
|
11
|
+
/** How often the listing is read while an operation or the frontend runs. */
|
|
12
|
+
const FOLLOW_INTERVAL_MS = 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The module store of the Modules page: the listing, the operations, and the
|
|
16
|
+
* follow-up that reads the listing again while something moves. An operation
|
|
17
|
+
* the owner started here reloads the page once the frontend serves its
|
|
18
|
+
* result, so a new module's pages appear without a manual refresh.
|
|
19
|
+
*/
|
|
20
|
+
export const useModuleStore = () => {
|
|
21
|
+
// Taken once, in setup: the follow-up reads the listing from a timer,
|
|
22
|
+
// outside any component context.
|
|
23
|
+
const { $authFetch } = useAuthFetch();
|
|
24
|
+
const listing = ref<StoreListing | undefined>(undefined);
|
|
25
|
+
const loadingError = ref<unknown>(null);
|
|
26
|
+
/** Modules an operation from this page changed, until the frontend follows. */
|
|
27
|
+
const awaitingReload = ref(new Set<string>());
|
|
28
|
+
let followTimer: ReturnType<typeof setTimeout> | undefined;
|
|
29
|
+
|
|
30
|
+
const isFollowing = computed(() => {
|
|
31
|
+
const modules = listing.value?.modules ?? [];
|
|
32
|
+
return (
|
|
33
|
+
listing.value?.busy === true ||
|
|
34
|
+
modules.some((entry) => isAwaitingFrontend(entry))
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function load(): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
listing.value = await $authFetch<StoreListing>(ENDPOINT_STORE_LISTING);
|
|
41
|
+
loadingError.value = null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
loadingError.value = error;
|
|
44
|
+
}
|
|
45
|
+
reloadWhenServed();
|
|
46
|
+
scheduleFollow();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reloads the page once every module an operation from here changed is
|
|
51
|
+
* served by the frontend (or has no interface at all). A frontend that
|
|
52
|
+
* never reported stays put: the page says what to run instead.
|
|
53
|
+
*/
|
|
54
|
+
function reloadWhenServed(): void {
|
|
55
|
+
if (import.meta.env.SSR || awaitingReload.value.size === 0) return;
|
|
56
|
+
const changed = (listing.value?.modules ?? []).filter((entry) =>
|
|
57
|
+
awaitingReload.value.has(entry.id),
|
|
58
|
+
);
|
|
59
|
+
const isSettled = changed.every(
|
|
60
|
+
(entry) =>
|
|
61
|
+
entry.state !== "busy" &&
|
|
62
|
+
(entry.frontend === "synced" || entry.frontend === "none"),
|
|
63
|
+
);
|
|
64
|
+
const needsReload = changed.some((entry) => entry.frontend === "synced");
|
|
65
|
+
if (!isSettled) return;
|
|
66
|
+
awaitingReload.value = new Set();
|
|
67
|
+
if (needsReload) window.location.reload();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scheduleFollow(): void {
|
|
71
|
+
if (import.meta.env.SSR) return;
|
|
72
|
+
if (followTimer) clearTimeout(followTimer);
|
|
73
|
+
followTimer = undefined;
|
|
74
|
+
if (!isFollowing.value) return;
|
|
75
|
+
followTimer = setTimeout(() => void load(), FOLLOW_INTERVAL_MS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Starts `kind` on `entry`, with `body` when it carries more than the id;
|
|
80
|
+
* rejects with the server's answer when refused.
|
|
81
|
+
*/
|
|
82
|
+
async function run(
|
|
83
|
+
kind: StoreOperationKind,
|
|
84
|
+
entry: StoreListingEntry,
|
|
85
|
+
body: StoreActionBody = { id: entry.id },
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
listing.value = await $authFetch<StoreListing>(
|
|
88
|
+
`${ENDPOINT_STORE_ACTION}/${kind}`,
|
|
89
|
+
{ method: "POST", body },
|
|
90
|
+
);
|
|
91
|
+
awaitingReload.value = new Set([...awaitingReload.value, entry.id]);
|
|
92
|
+
scheduleFollow();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stop(): void {
|
|
96
|
+
if (followTimer) clearTimeout(followTimer);
|
|
97
|
+
followTimer = undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
listing,
|
|
102
|
+
loadingError,
|
|
103
|
+
isFollowing,
|
|
104
|
+
awaitingReload,
|
|
105
|
+
load,
|
|
106
|
+
run,
|
|
107
|
+
stop,
|
|
108
|
+
};
|
|
109
|
+
};
|