@antelopejs/dms 0.9.0 → 0.9.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/CHANGELOG.md +34 -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/implementations/dms-auth/index.js +1 -1
- package/dist/implementations/dms-auth/index.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/interfaces/dms/page-extension-ownership.test.js +1 -1
- package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js.map +1 -1
- 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/test/unit/utils/user-activity.test.js +4 -12
- package/dist/test/unit/utils/user-activity.test.js.map +1 -1
- package/dist/utils/user-activity.d.ts +1 -1
- package/dist/utils/user-activity.js +6 -3
- package/dist/utils/user-activity.js.map +1 -1
- 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 +5 -5
|
@@ -5,11 +5,30 @@ import {
|
|
|
5
5
|
moduleCategoryOptions,
|
|
6
6
|
} from "../layers/dms-layout/app/build/utils/modules-catalog";
|
|
7
7
|
import {
|
|
8
|
-
MODULE_STORE_CATALOG,
|
|
9
|
-
availableStoreModules,
|
|
10
|
-
moduleInstallCommand,
|
|
11
8
|
type ModuleStoreEntry,
|
|
9
|
+
filterByScope,
|
|
10
|
+
isAwaitingFrontend,
|
|
11
|
+
needsDependencyChoice,
|
|
12
|
+
sortStoreModules,
|
|
13
|
+
storeActionBlock,
|
|
14
|
+
storeActions,
|
|
15
|
+
storeTileStatus,
|
|
16
|
+
toModuleStoreEntry,
|
|
12
17
|
} from "../layers/dms-layout/app/build/utils/module-store-catalog";
|
|
18
|
+
import {
|
|
19
|
+
OTHER_PACKAGE_CHOICE,
|
|
20
|
+
areDependencyDraftsValid,
|
|
21
|
+
buildInstallBody,
|
|
22
|
+
createDependencyDrafts,
|
|
23
|
+
isNpmPackageName,
|
|
24
|
+
parseDependencyConfig,
|
|
25
|
+
validateDependencyDraft,
|
|
26
|
+
} from "../layers/dms-layout/app/build/utils/module-store-install";
|
|
27
|
+
import type {
|
|
28
|
+
MissingInterface,
|
|
29
|
+
StoreListingEntry,
|
|
30
|
+
} from "../layers/dms-layout/app/types/module-store";
|
|
31
|
+
import catalogDocument from "../../data/module-store/catalog.json";
|
|
13
32
|
import en from "../layers/dms-layout/i18n/locales/layout-en-GB.json";
|
|
14
33
|
import fr from "../layers/dms-layout/i18n/locales/layout-fr-FR.json";
|
|
15
34
|
|
|
@@ -33,16 +52,33 @@ const resolve = (value: string): string => {
|
|
|
33
52
|
return typeof text === "string" ? text : value;
|
|
34
53
|
};
|
|
35
54
|
|
|
36
|
-
function
|
|
55
|
+
function listingEntry(
|
|
37
56
|
id: string,
|
|
38
|
-
overrides: Partial<
|
|
39
|
-
):
|
|
57
|
+
overrides: Partial<StoreListingEntry> = {},
|
|
58
|
+
): StoreListingEntry {
|
|
40
59
|
return {
|
|
41
60
|
id,
|
|
42
|
-
|
|
61
|
+
package: `@antelopejs/dms-${id}`,
|
|
62
|
+
icon: "i-ph-cube",
|
|
63
|
+
category: "data",
|
|
43
64
|
title: id[0].toUpperCase() + id.slice(1),
|
|
44
65
|
description: `${id} description`,
|
|
45
|
-
|
|
66
|
+
state: "available",
|
|
67
|
+
updateAvailable: false,
|
|
68
|
+
compatible: true,
|
|
69
|
+
frontend: "none",
|
|
70
|
+
missingInterfaces: [],
|
|
71
|
+
dependents: [],
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function storeEntry(
|
|
77
|
+
id: string,
|
|
78
|
+
overrides: Partial<ModuleStoreEntry> = {},
|
|
79
|
+
): ModuleStoreEntry {
|
|
80
|
+
return {
|
|
81
|
+
...toModuleStoreEntry(listingEntry(id)),
|
|
46
82
|
catalogCategory: "Data",
|
|
47
83
|
...overrides,
|
|
48
84
|
};
|
|
@@ -53,32 +89,95 @@ const catalog = [
|
|
|
53
89
|
catalogCategory: "Content",
|
|
54
90
|
description: "Asset library",
|
|
55
91
|
}),
|
|
56
|
-
storeEntry("database"),
|
|
92
|
+
storeEntry("database", { state: "active", installedVersion: "0.1.2" }),
|
|
57
93
|
storeEntry("lang", { title: "Translations", catalogCategory: "Content" }),
|
|
58
|
-
storeEntry("ai", {
|
|
94
|
+
storeEntry("ai", {
|
|
95
|
+
catalogCategory: "AI",
|
|
96
|
+
state: "disabled",
|
|
97
|
+
installedVersion: "0.2.1",
|
|
98
|
+
}),
|
|
59
99
|
];
|
|
60
100
|
|
|
101
|
+
const actionKinds = (entry: StoreListingEntry) =>
|
|
102
|
+
storeActions(entry).map((action) => action.kind);
|
|
103
|
+
|
|
61
104
|
describe("module store catalog", () => {
|
|
62
|
-
it("
|
|
63
|
-
|
|
64
|
-
catalog,
|
|
65
|
-
|
|
66
|
-
resolve,
|
|
67
|
-
);
|
|
68
|
-
expect(available.map((entry) => entry.id)).toEqual(["ai", "media", "lang"]);
|
|
105
|
+
it("sorts the modules by their shown name", () => {
|
|
106
|
+
expect(
|
|
107
|
+
sortStoreModules(catalog, resolve).map((entry) => entry.title),
|
|
108
|
+
).toEqual(["Ai", "Database", "Media", "Translations"]);
|
|
69
109
|
});
|
|
70
110
|
|
|
71
|
-
it("
|
|
111
|
+
it("splits installed modules, paused ones included, from available ones", () => {
|
|
72
112
|
expect(
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
filterByScope(catalog, "installed").map((entry) => entry.id),
|
|
114
|
+
).toEqual(["database", "ai"]);
|
|
115
|
+
expect(
|
|
116
|
+
filterByScope(catalog, "available").map((entry) => entry.id),
|
|
117
|
+
).toEqual(["media", "lang"]);
|
|
118
|
+
expect(filterByScope(catalog, "all")).toHaveLength(4);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("counts a failed install as available, a failed update as installed", () => {
|
|
122
|
+
const failedInstall = listingEntry("api", { state: "error" });
|
|
123
|
+
const failedUpdate = listingEntry("api", {
|
|
124
|
+
state: "error",
|
|
125
|
+
installedVersion: "0.3.0",
|
|
126
|
+
});
|
|
127
|
+
expect(filterByScope([failedInstall], "available")).toHaveLength(1);
|
|
128
|
+
expect(filterByScope([failedUpdate], "installed")).toHaveLength(1);
|
|
129
|
+
expect(actionKinds(failedInstall)).toEqual(["install"]);
|
|
130
|
+
expect(actionKinds(failedUpdate)).toEqual(["apply", "uninstall"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("offers each state its actions, the main one first", () => {
|
|
134
|
+
expect(actionKinds(listingEntry("a"))).toEqual(["install"]);
|
|
135
|
+
expect(actionKinds(listingEntry("a", { state: "active" }))).toEqual([
|
|
136
|
+
"disable",
|
|
137
|
+
"uninstall",
|
|
138
|
+
]);
|
|
139
|
+
expect(
|
|
140
|
+
actionKinds(
|
|
141
|
+
listingEntry("a", { state: "active", updateAvailable: true }),
|
|
75
142
|
),
|
|
76
|
-
).toEqual(["
|
|
143
|
+
).toEqual(["update", "disable", "uninstall"]);
|
|
144
|
+
expect(actionKinds(listingEntry("a", { state: "disabled" }))).toEqual([
|
|
145
|
+
"enable",
|
|
146
|
+
"uninstall",
|
|
147
|
+
]);
|
|
148
|
+
expect(actionKinds(listingEntry("a", { state: "busy" }))).toEqual([]);
|
|
77
149
|
});
|
|
78
150
|
|
|
79
|
-
it("
|
|
80
|
-
const
|
|
81
|
-
|
|
151
|
+
it("asks for confirmation before uninstalling only", () => {
|
|
152
|
+
const confirmed = storeActions(listingEntry("a", { state: "disabled" }))
|
|
153
|
+
.filter((action) => action.confirm)
|
|
154
|
+
.map((action) => action.kind);
|
|
155
|
+
expect(confirmed).toEqual(["uninstall"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("flags an update on an active module's pill", () => {
|
|
159
|
+
expect(
|
|
160
|
+
storeTileStatus(
|
|
161
|
+
listingEntry("a", { state: "active", updateAvailable: true }),
|
|
162
|
+
).labelKey,
|
|
163
|
+
).toBe("update_available");
|
|
164
|
+
expect(storeTileStatus(listingEntry("a", { state: "busy" })).dot).toBe(
|
|
165
|
+
"live",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("waits on the frontend only once the operation is over", () => {
|
|
170
|
+
expect(isAwaitingFrontend(listingEntry("a", { frontend: "pending" }))).toBe(
|
|
171
|
+
true,
|
|
172
|
+
);
|
|
173
|
+
expect(
|
|
174
|
+
isAwaitingFrontend(
|
|
175
|
+
listingEntry("a", { state: "busy", frontend: "pending" }),
|
|
176
|
+
),
|
|
177
|
+
).toBe(false);
|
|
178
|
+
expect(isAwaitingFrontend(listingEntry("a", { frontend: "unknown" }))).toBe(
|
|
179
|
+
false,
|
|
180
|
+
);
|
|
82
181
|
});
|
|
83
182
|
|
|
84
183
|
it("searches the name, description, category and id", () => {
|
|
@@ -95,23 +194,6 @@ describe("module store catalog", () => {
|
|
|
95
194
|
expect(search("payments")).toEqual([]);
|
|
96
195
|
});
|
|
97
196
|
|
|
98
|
-
it("filters by category, combined with the search", () => {
|
|
99
|
-
expect(
|
|
100
|
-
filterModules(catalog, {
|
|
101
|
-
query: "",
|
|
102
|
-
category: "Content",
|
|
103
|
-
resolve,
|
|
104
|
-
}).map((entry) => entry.id),
|
|
105
|
-
).toEqual(["media", "lang"]);
|
|
106
|
-
expect(
|
|
107
|
-
filterModules(catalog, {
|
|
108
|
-
query: "lang",
|
|
109
|
-
category: "Data",
|
|
110
|
-
resolve,
|
|
111
|
-
}),
|
|
112
|
-
).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
197
|
it("offers the categories present, with their counts", () => {
|
|
116
198
|
expect(moduleCategoryOptions(catalog, resolve)).toEqual([
|
|
117
199
|
{ value: "AI", label: "AI", count: 1 },
|
|
@@ -119,31 +201,168 @@ describe("module store catalog", () => {
|
|
|
119
201
|
{ value: "Data", label: "Data", count: 1 },
|
|
120
202
|
]);
|
|
121
203
|
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const REDIS: MissingInterface = {
|
|
207
|
+
interfacePackage: "@antelopejs/interface-redis",
|
|
208
|
+
candidates: [
|
|
209
|
+
{ name: "redis", package: "@antelopejs/redis" },
|
|
210
|
+
{ name: "valkey", package: "@antelopejs/valkey" },
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
const QUEUE: MissingInterface = {
|
|
214
|
+
interfacePackage: "@antelopejs/interface-queue",
|
|
215
|
+
candidates: [],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const FRONTEND_REQUIREMENT = {
|
|
219
|
+
range: ">=0.7.0",
|
|
220
|
+
running: "0.6.0",
|
|
221
|
+
target: "0.7.2",
|
|
222
|
+
command: "pnpm add @antelopejs/dms-frontend@0.7.2",
|
|
223
|
+
breaks: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
describe("module store blocked actions", () => {
|
|
227
|
+
const released = (overrides: Partial<StoreListingEntry> = {}) =>
|
|
228
|
+
listingEntry("cache", { latestVersion: "1.0.0", ...overrides });
|
|
229
|
+
|
|
230
|
+
it("lets a releasable module install and update", () => {
|
|
231
|
+
expect(storeActionBlock(released(), "install")).toBeUndefined();
|
|
232
|
+
expect(storeActionBlock(released(), "update")).toBeUndefined();
|
|
233
|
+
});
|
|
122
234
|
|
|
123
|
-
it("
|
|
124
|
-
expect(
|
|
125
|
-
"
|
|
235
|
+
it("blocks a release without a version, on another interface or dms-frontend", () => {
|
|
236
|
+
expect(storeActionBlock(listingEntry("cache"), "install")).toBe(
|
|
237
|
+
"no_release",
|
|
238
|
+
);
|
|
239
|
+
expect(storeActionBlock(released({ compatible: false }), "update")).toBe(
|
|
240
|
+
"incompatible",
|
|
126
241
|
);
|
|
242
|
+
expect(
|
|
243
|
+
storeActionBlock(
|
|
244
|
+
released({ frontendRequirement: FRONTEND_REQUIREMENT }),
|
|
245
|
+
"install",
|
|
246
|
+
),
|
|
247
|
+
).toBe("frontend");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("asks for the missing interfaces on install, refuses them on update", () => {
|
|
251
|
+
const missing = released({ missingInterfaces: [REDIS] });
|
|
252
|
+
expect(storeActionBlock(missing, "install")).toBeUndefined();
|
|
253
|
+
expect(needsDependencyChoice(missing, "install")).toBe(true);
|
|
254
|
+
expect(storeActionBlock(missing, "update")).toBe("interfaces");
|
|
255
|
+
expect(needsDependencyChoice(missing, "update")).toBe(false);
|
|
256
|
+
expect(needsDependencyChoice(released(), "install")).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("keeps a module others need from being paused or removed", () => {
|
|
260
|
+
const needed = released({ state: "active", dependents: ["@acme/jobs"] });
|
|
261
|
+
expect(storeActionBlock(needed, "disable")).toBe("dependents");
|
|
262
|
+
expect(storeActionBlock(needed, "uninstall")).toBe("dependents");
|
|
263
|
+
expect(storeActionBlock(needed, "enable")).toBeUndefined();
|
|
264
|
+
expect(
|
|
265
|
+
storeActionBlock(released({ state: "active" }), "uninstall"),
|
|
266
|
+
).toBeUndefined();
|
|
127
267
|
});
|
|
128
268
|
});
|
|
129
269
|
|
|
130
|
-
describe("
|
|
270
|
+
describe("module store install dialog", () => {
|
|
271
|
+
it("accepts npm package names only", () => {
|
|
272
|
+
expect(isNpmPackageName("redis")).toBe(true);
|
|
273
|
+
expect(isNpmPackageName("@antelopejs/redis")).toBe(true);
|
|
274
|
+
expect(isNpmPackageName("Redis")).toBe(false);
|
|
275
|
+
expect(isNpmPackageName("git+https://github.com/a/b.git")).toBe(false);
|
|
276
|
+
expect(isNpmPackageName("./local/module")).toBe(false);
|
|
277
|
+
expect(isNpmPackageName("@scope")).toBe(false);
|
|
278
|
+
expect(isNpmPackageName("a".repeat(215))).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("reads an empty config as none and refuses anything but an object", () => {
|
|
282
|
+
expect(parseDependencyConfig(" ")).toEqual({ isValid: true });
|
|
283
|
+
expect(parseDependencyConfig('{ "url": "redis://cache" }')).toEqual({
|
|
284
|
+
isValid: true,
|
|
285
|
+
config: { url: "redis://cache" },
|
|
286
|
+
});
|
|
287
|
+
expect(parseDependencyConfig("[]").isValid).toBe(false);
|
|
288
|
+
expect(parseDependencyConfig("null").isValid).toBe(false);
|
|
289
|
+
expect(parseDependencyConfig("{ url: }").isValid).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("preselects the first candidate, or the other package without one", () => {
|
|
293
|
+
expect(
|
|
294
|
+
createDependencyDrafts([REDIS, QUEUE]).map((draft) => draft.choice),
|
|
295
|
+
).toEqual(["@antelopejs/redis", OTHER_PACKAGE_CHOICE]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps what an earlier attempt picked for the same interface", () => {
|
|
299
|
+
const [earlier] = createDependencyDrafts([REDIS]);
|
|
300
|
+
const edited = { ...earlier!, choice: "@antelopejs/valkey", config: "{}" };
|
|
301
|
+
expect(createDependencyDrafts([REDIS, QUEUE], [edited])[0]).toEqual(edited);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("flags a missing or invalid package and a config that is not an object", () => {
|
|
305
|
+
const [draft] = createDependencyDrafts([QUEUE]);
|
|
306
|
+
expect(validateDependencyDraft(draft!)).toEqual({
|
|
307
|
+
package: "package_required",
|
|
308
|
+
});
|
|
309
|
+
expect(
|
|
310
|
+
validateDependencyDraft({
|
|
311
|
+
...draft!,
|
|
312
|
+
otherPackage: "github:acme/queue",
|
|
313
|
+
config: "[1]",
|
|
314
|
+
}),
|
|
315
|
+
).toEqual({ package: "package_invalid", config: "config_invalid" });
|
|
316
|
+
expect(
|
|
317
|
+
areDependencyDraftsValid([{ ...draft!, otherPackage: " @acme/queue " }]),
|
|
318
|
+
).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("builds the install body, leaving out an empty config", () => {
|
|
322
|
+
const [redis, queue] = createDependencyDrafts([REDIS, QUEUE]);
|
|
323
|
+
expect(
|
|
324
|
+
buildInstallBody("cache", [
|
|
325
|
+
redis!,
|
|
326
|
+
{ ...queue!, otherPackage: "@acme/queue", config: '{"size":2}' },
|
|
327
|
+
]),
|
|
328
|
+
).toEqual({
|
|
329
|
+
id: "cache",
|
|
330
|
+
dependencies: [
|
|
331
|
+
{
|
|
332
|
+
interfacePackage: "@antelopejs/interface-redis",
|
|
333
|
+
package: "@antelopejs/redis",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
interfacePackage: "@antelopejs/interface-queue",
|
|
337
|
+
package: "@acme/queue",
|
|
338
|
+
config: { size: 2 },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
expect(buildInstallBody("cache", [])).toEqual({ id: "cache" });
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("bundled module catalog", () => {
|
|
347
|
+
const entries = catalogDocument.modules.map((entry) =>
|
|
348
|
+
toModuleStoreEntry(listingEntry(entry.id, { category: entry.category })),
|
|
349
|
+
);
|
|
350
|
+
|
|
131
351
|
it("has unique ids and packages", () => {
|
|
132
|
-
const ids =
|
|
133
|
-
const packages =
|
|
352
|
+
const ids = catalogDocument.modules.map((entry) => entry.id);
|
|
353
|
+
const packages = catalogDocument.modules.map((entry) => entry.package);
|
|
134
354
|
expect(new Set(ids).size).toBe(ids.length);
|
|
135
355
|
expect(new Set(packages).size).toBe(packages.length);
|
|
136
356
|
});
|
|
137
357
|
|
|
138
358
|
it("translates every name, description and category in both locales", () => {
|
|
139
359
|
for (const messages of [en, fr]) {
|
|
140
|
-
for (const entry of
|
|
360
|
+
for (const entry of entries) {
|
|
141
361
|
for (const key of [
|
|
142
|
-
entry.title
|
|
143
|
-
entry.description
|
|
362
|
+
`$modules.store.catalog.${entry.id}.title`,
|
|
363
|
+
`$modules.store.catalog.${entry.id}.description`,
|
|
144
364
|
entry.catalogCategory,
|
|
145
365
|
]) {
|
|
146
|
-
expect(key.startsWith("$")).toBe(true);
|
|
147
366
|
expect(typeof lookup(messages, key.slice(1)), key).toBe("string");
|
|
148
367
|
}
|
|
149
368
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A TopListCard's period badge across a server render and its hydration,
|
|
4
|
+
* through the runtime's real payload: once its selector set up before it, the
|
|
5
|
+
* server prints the preset whatever it knows of the browser's time zone (a
|
|
6
|
+
* preset reads the same in every zone, and a relative one at every instant),
|
|
7
|
+
* and the browser hydrates it without a mismatch. A selector further down the
|
|
8
|
+
* page leaves a skeleton on both sides, which the label replaces once mounted.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
createSSRApp,
|
|
12
|
+
defineComponent,
|
|
13
|
+
h,
|
|
14
|
+
nextTick,
|
|
15
|
+
onBeforeUnmount,
|
|
16
|
+
ref,
|
|
17
|
+
type App,
|
|
18
|
+
type Component,
|
|
19
|
+
} from "vue";
|
|
20
|
+
import { renderToString } from "vue/server-renderer";
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
22
|
+
import type { PeriodPreset } from "../layers/dms-core/app/composables/period/types";
|
|
23
|
+
import { browserTimeZone } from "../layers/dms-core/app/utils/regional";
|
|
24
|
+
import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
|
|
25
|
+
|
|
26
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
27
|
+
useChartFetch: () => ({
|
|
28
|
+
data: ref({ items: [{ id: "pro", title: "Pro", value: 12 }] }),
|
|
29
|
+
isLoading: ref(false),
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const engine = await import("#dms/frontend-module");
|
|
34
|
+
const { usePeriod } = await import(
|
|
35
|
+
"../layers/dms-core/app/composables/period/usePeriod"
|
|
36
|
+
);
|
|
37
|
+
const { registerPeriodScope } = await import(
|
|
38
|
+
"../layers/dms-core/app/composables/period/usePeriodScope"
|
|
39
|
+
);
|
|
40
|
+
const { TIME_ZONE_COOKIE } = await import(
|
|
41
|
+
"../layers/dms-core/app/composables/period/periodTimeZone"
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const SCOPE = "dashboard";
|
|
45
|
+
const SKELETON = "SKELETON";
|
|
46
|
+
|
|
47
|
+
function selector(preset: PeriodPreset): Component {
|
|
48
|
+
return defineComponent({
|
|
49
|
+
setup() {
|
|
50
|
+
const period = usePeriod({ defaultPreset: preset });
|
|
51
|
+
onBeforeUnmount(registerPeriodScope(SCOPE, period.state));
|
|
52
|
+
return () => h("i");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A skeleton carries an aria-label; the badge renders its label.
|
|
58
|
+
const Slot = defineComponent({
|
|
59
|
+
props: { ariaLabel: String },
|
|
60
|
+
setup:
|
|
61
|
+
(props, { slots }) =>
|
|
62
|
+
() =>
|
|
63
|
+
h("span", props.ariaLabel ? SKELETON : slots.default?.()),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const Card = defineComponent({
|
|
67
|
+
setup: () => () =>
|
|
68
|
+
h(TopListCard, {
|
|
69
|
+
title: "Top plans",
|
|
70
|
+
componentId: "plans",
|
|
71
|
+
pageId: "dashboard",
|
|
72
|
+
fetchUrl: "/api/plans/top",
|
|
73
|
+
periodScope: SCOPE,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function page(children: Component[]): App {
|
|
78
|
+
const app = createSSRApp({
|
|
79
|
+
render: () =>
|
|
80
|
+
h(
|
|
81
|
+
"main",
|
|
82
|
+
children.map((child) => h(child)),
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
for (const name of ["DmsCard", "UBadge", "USkeleton", "DmsTopListRow"])
|
|
86
|
+
app.component(name, Slot);
|
|
87
|
+
return app;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function zoneCookie(zone: string): string {
|
|
91
|
+
return `${TIME_ZONE_COOKIE}=${encodeURIComponent(JSON.stringify(zone))}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function serverRender(children: Component[], cookies?: string) {
|
|
95
|
+
const runtime = engine.createDmsFrontendRuntime(undefined, {}, true, cookies);
|
|
96
|
+
const app = page(children);
|
|
97
|
+
engine.provideDmsFrontendRuntime(app, runtime);
|
|
98
|
+
engine.setDmsServerRuntimeResolver(() => runtime);
|
|
99
|
+
// What the server sees: no `window`.
|
|
100
|
+
const browserWindow = globalThis.window;
|
|
101
|
+
vi.stubGlobal("window", undefined);
|
|
102
|
+
try {
|
|
103
|
+
const html = await renderToString(app);
|
|
104
|
+
return { html, payload: engine.serializeDmsPayload(runtime) };
|
|
105
|
+
} finally {
|
|
106
|
+
vi.stubGlobal("window", browserWindow);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const mounted: App[] = [];
|
|
111
|
+
|
|
112
|
+
async function hydrate(
|
|
113
|
+
children: Component[],
|
|
114
|
+
{ html, payload }: { html: string; payload: unknown },
|
|
115
|
+
) {
|
|
116
|
+
const browser = engine.createDmsFrontendRuntime(undefined, {});
|
|
117
|
+
engine.restoreDmsPayload(
|
|
118
|
+
browser,
|
|
119
|
+
structuredClone(payload) as Parameters<typeof engine.restoreDmsPayload>[1],
|
|
120
|
+
);
|
|
121
|
+
const container = document.createElement("div");
|
|
122
|
+
container.innerHTML = html;
|
|
123
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
124
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
125
|
+
try {
|
|
126
|
+
const app = page(children);
|
|
127
|
+
engine.provideDmsFrontendRuntime(app, browser);
|
|
128
|
+
app.mount(container);
|
|
129
|
+
mounted.push(app);
|
|
130
|
+
await nextTick();
|
|
131
|
+
const mismatches = [...warn.mock.calls, ...error.mock.calls].filter(
|
|
132
|
+
(call) => /hydration|mismatch/i.test(String(call[0])),
|
|
133
|
+
);
|
|
134
|
+
return { container, mismatches };
|
|
135
|
+
} finally {
|
|
136
|
+
warn.mockRestore();
|
|
137
|
+
error.mockRestore();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The card head's badge: its preset label, or its skeleton. */
|
|
142
|
+
function badge(html: string): string {
|
|
143
|
+
const label = /dms\.period\.presets\.[a-z0-9-]+/.exec(html)?.[0];
|
|
144
|
+
if (label) return label;
|
|
145
|
+
return html.includes(SKELETON) ? SKELETON : "none";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
|
|
150
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
151
|
+
processI18n: (text: string) => text,
|
|
152
|
+
}));
|
|
153
|
+
vi.stubGlobal("useComponentEvent", () => ({}));
|
|
154
|
+
vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
|
|
155
|
+
vi.stubGlobal("useDmsState", engine.useDmsState);
|
|
156
|
+
vi.stubGlobal("useDmsCookie", engine.useDmsCookie);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
161
|
+
vi.unstubAllGlobals();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("a TopListCard's period badge across hydration", () => {
|
|
165
|
+
it("is in the server's HTML, and hydrates without a mismatch", async () => {
|
|
166
|
+
const children = [selector("this-month"), Card];
|
|
167
|
+
const rendered = await serverRender(
|
|
168
|
+
children,
|
|
169
|
+
zoneCookie(browserTimeZone()),
|
|
170
|
+
);
|
|
171
|
+
expect(badge(rendered.html)).toBe("dms.period.presets.this-month");
|
|
172
|
+
|
|
173
|
+
const { container, mismatches } = await hydrate(children, rendered);
|
|
174
|
+
expect(mismatches).toEqual([]);
|
|
175
|
+
expect(badge(container.innerHTML)).toBe("dms.period.presets.this-month");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("is rendered before the browser's time zone is known", async () => {
|
|
179
|
+
const children = [selector("this-month"), Card];
|
|
180
|
+
const rendered = await serverRender(children);
|
|
181
|
+
expect(badge(rendered.html)).toBe("dms.period.presets.this-month");
|
|
182
|
+
|
|
183
|
+
const { mismatches } = await hydrate(children, rendered);
|
|
184
|
+
expect(mismatches).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("names a relative preset too", async () => {
|
|
188
|
+
const children = [selector("last-24h"), Card];
|
|
189
|
+
const rendered = await serverRender(
|
|
190
|
+
children,
|
|
191
|
+
zoneCookie(browserTimeZone()),
|
|
192
|
+
);
|
|
193
|
+
expect(badge(rendered.html)).toBe("dms.period.presets.last-24h");
|
|
194
|
+
|
|
195
|
+
const { mismatches } = await hydrate(children, rendered);
|
|
196
|
+
expect(mismatches).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("waits for a selector set up after the card, on both sides", async () => {
|
|
200
|
+
const children = [Card, selector("last-year")];
|
|
201
|
+
const rendered = await serverRender(
|
|
202
|
+
children,
|
|
203
|
+
zoneCookie(browserTimeZone()),
|
|
204
|
+
);
|
|
205
|
+
expect(badge(rendered.html)).toBe(SKELETON);
|
|
206
|
+
|
|
207
|
+
const { container, mismatches } = await hydrate(children, rendered);
|
|
208
|
+
expect(mismatches).toEqual([]);
|
|
209
|
+
await nextTick();
|
|
210
|
+
expect(badge(container.innerHTML)).toBe("dms.period.presets.last-year");
|
|
211
|
+
});
|
|
212
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AntelopeJS/dms.git"
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"2fa": "^0.1.2",
|
|
34
34
|
"@antelopejs/interface-api": ">=0.0.13 <1.0.0",
|
|
35
35
|
"@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
|
|
36
|
-
"@antelopejs/interface-core": ">=0.
|
|
36
|
+
"@antelopejs/interface-core": ">=0.2.3 <1.0.0",
|
|
37
37
|
"@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
|
|
38
38
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
39
39
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
40
|
-
"@antelopejs/interface-dms": ">=0.7.
|
|
40
|
+
"@antelopejs/interface-dms": ">=0.7.1 <0.8.0",
|
|
41
41
|
"@antelopejs/interface-email": ">=0.0.5 <1.0.0",
|
|
42
42
|
"@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
|
|
43
43
|
"archiver": "^7.0.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"zod": "~3.24.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@antelopejs/core": ">=1.
|
|
57
|
+
"@antelopejs/core": ">=1.15.2 <2",
|
|
58
58
|
"@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
|
|
59
59
|
"@types/archiver": "^6.0.4",
|
|
60
60
|
"@types/chai": "^4.3.20",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@antelopejs/interface-redis": ">=0.0.8 <1.0.0"
|
|
97
97
|
},
|
|
98
98
|
"peerDependencies": {
|
|
99
|
-
"@antelopejs/core": ">=1.
|
|
99
|
+
"@antelopejs/core": ">=1.15.2 <2"
|
|
100
100
|
},
|
|
101
101
|
"peerDependenciesMeta": {
|
|
102
102
|
"@antelopejs/core": {
|