@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/data/module-store/catalog.json +71 -0
  3. package/dist/config.d.ts +7 -0
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/implementations/dms/frontend-modules.d.ts +2 -0
  7. package/dist/implementations/dms/frontend-modules.js +29 -1
  8. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  9. package/dist/implementations/dms/frontend-ready.d.ts +8 -0
  10. package/dist/implementations/dms/frontend-ready.js +34 -0
  11. package/dist/implementations/dms/frontend-ready.js.map +1 -0
  12. package/dist/implementations/dms/manifest.d.ts +1 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/page.d.ts +2 -0
  15. package/dist/implementations/dms/page.js +16 -0
  16. package/dist/implementations/dms/page.js.map +1 -1
  17. package/dist/routes/index.d.ts +1 -0
  18. package/dist/routes/index.js +1 -0
  19. package/dist/routes/index.js.map +1 -1
  20. package/dist/routes/store.d.ts +21 -0
  21. package/dist/routes/store.js +130 -0
  22. package/dist/routes/store.js.map +1 -0
  23. package/dist/store/activity.d.ts +2 -0
  24. package/dist/store/activity.js +12 -0
  25. package/dist/store/activity.js.map +1 -0
  26. package/dist/store/cache.d.ts +8 -0
  27. package/dist/store/cache.js +29 -0
  28. package/dist/store/cache.js.map +1 -0
  29. package/dist/store/catalog.d.ts +4 -0
  30. package/dist/store/catalog.js +69 -0
  31. package/dist/store/catalog.js.map +1 -0
  32. package/dist/store/compatibility.d.ts +3 -0
  33. package/dist/store/compatibility.js +45 -0
  34. package/dist/store/compatibility.js.map +1 -0
  35. package/dist/store/declarations.d.ts +18 -0
  36. package/dist/store/declarations.js +116 -0
  37. package/dist/store/declarations.js.map +1 -0
  38. package/dist/store/frontend-compatibility.d.ts +18 -0
  39. package/dist/store/frontend-compatibility.js +93 -0
  40. package/dist/store/frontend-compatibility.js.map +1 -0
  41. package/dist/store/interfaces.d.ts +6 -0
  42. package/dist/store/interfaces.js +126 -0
  43. package/dist/store/interfaces.js.map +1 -0
  44. package/dist/store/listing.d.ts +2 -0
  45. package/dist/store/listing.js +180 -0
  46. package/dist/store/listing.js.map +1 -0
  47. package/dist/store/operations.d.ts +8 -0
  48. package/dist/store/operations.js +254 -0
  49. package/dist/store/operations.js.map +1 -0
  50. package/dist/store/registry.d.ts +4 -0
  51. package/dist/store/registry.js +61 -0
  52. package/dist/store/registry.js.map +1 -0
  53. package/dist/store/requirements.d.ts +14 -0
  54. package/dist/store/requirements.js +81 -0
  55. package/dist/store/requirements.js.map +1 -0
  56. package/dist/store/runtime.d.ts +11 -0
  57. package/dist/store/runtime.js +78 -0
  58. package/dist/store/runtime.js.map +1 -0
  59. package/dist/store/tarball.d.ts +2 -0
  60. package/dist/store/tarball.js +80 -0
  61. package/dist/store/tarball.js.map +1 -0
  62. package/dist/store/types.d.ts +98 -0
  63. package/dist/store/types.js +3 -0
  64. package/dist/store/types.js.map +1 -0
  65. package/dist/test/unit/store/store.test.d.ts +1 -0
  66. package/dist/test/unit/store/store.test.js +127 -0
  67. package/dist/test/unit/store/store.test.js.map +1 -0
  68. package/dist/validation/store.schema.d.ts +39 -0
  69. package/dist/validation/store.schema.js +27 -0
  70. package/dist/validation/store.schema.js.map +1 -0
  71. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  72. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  73. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  74. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  75. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  76. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  77. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  78. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +335 -34
  79. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  80. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  81. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  82. package/frontend-vue/package.json +3 -3
  83. package/frontend-vue/pnpm-lock.yaml +16 -22
  84. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  85. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  86. package/package.json +1 -1
@@ -1,23 +1,175 @@
1
1
  <script setup lang="ts">
2
2
  import ModuleTile from "./ModuleTile.vue";
3
+ import ModuleStoreRequirements from "./ModuleStoreRequirements.vue";
3
4
  import {
4
- MODULE_INSTALL_COMMAND,
5
- moduleInstallCommand,
6
5
  type ModuleStoreEntry,
6
+ type StoreAction,
7
+ type StoreActionBlock,
8
+ storeActionBlock,
9
+ storeActions,
10
+ storeTileStatus,
7
11
  } from "../../../utils/module-store-catalog";
12
+ import type {
13
+ StoreOperationKind,
14
+ StoreOperationStep,
15
+ } from "../../../../types/module-store";
8
16
 
9
17
  interface ModuleStoreTileProps {
10
18
  /** The store module the tile presents. */
11
19
  entry: ModuleStoreEntry;
20
+ /** Another module's operation runs: every action waits for it. */
21
+ isLocked?: boolean;
12
22
  }
13
23
 
14
- const props = defineProps<ModuleStoreTileProps>();
24
+ interface ModuleStoreTileEmits {
25
+ action: [kind: StoreOperationKind];
26
+ }
27
+
28
+ const props = withDefaults(defineProps<ModuleStoreTileProps>(), {
29
+ isLocked: false,
30
+ });
31
+ const emit = defineEmits<ModuleStoreTileEmits>();
15
32
  const { t } = useI18n();
16
33
  const { processI18n } = useTranslation();
17
34
 
18
35
  const TOOLTIP_CONTENT = { side: "top", sideOffset: 6 } as const;
36
+ /** The steps an operation shows, in the order they run. */
37
+ const PROGRESS_STEPS: StoreOperationStep[] = [
38
+ "resolve",
39
+ "dependencies",
40
+ "unload",
41
+ "load",
42
+ "save",
43
+ ];
44
+ /**
45
+ * Steps an operation of this kind goes through. The file is written last:
46
+ * it only ever names a module that loaded.
47
+ */
48
+ const KIND_STEPS: Record<StoreOperationKind, StoreOperationStep[]> = {
49
+ install: ["resolve", "dependencies", "load", "save"],
50
+ update: ["resolve", "unload", "load", "save"],
51
+ enable: ["load", "save"],
52
+ disable: ["unload", "save"],
53
+ uninstall: ["unload", "save"],
54
+ apply: ["load"],
55
+ };
56
+
57
+ const status = computed(() => storeTileStatus(props.entry));
58
+ const actions = computed(() => storeActions(props.entry));
59
+ const primary = computed<StoreAction | undefined>(() =>
60
+ actions.value.find((action) => !action.isMenuOnly),
61
+ );
62
+ const secondary = computed(() =>
63
+ actions.value
64
+ .filter((action) => action !== primary.value)
65
+ .map((action) => ({
66
+ label: t(`modules.store.actions.${action.labelKey}`),
67
+ icon: action.icon,
68
+ color: action.kind === "uninstall" ? ("error" as const) : undefined,
69
+ disabled: storeActionBlock(props.entry, action.kind) !== undefined,
70
+ onSelect: () => emit("action", action.kind),
71
+ })),
72
+ );
73
+
74
+ const isInstalledHere = computed(
75
+ () => props.entry.installedVersion !== undefined,
76
+ );
77
+ const LIST_SEPARATOR = ", ";
78
+
79
+ const BLOCK_MESSAGES: Record<StoreActionBlock, () => string> = {
80
+ no_release: () => t("modules.store.no_release"),
81
+ incompatible: () =>
82
+ t("modules.store.incompatible", {
83
+ range: props.entry.requiredInterfaceRange ?? "",
84
+ }),
85
+ frontend: () =>
86
+ t("modules.store.frontend_requirement.needs", {
87
+ range: props.entry.frontendRequirement?.range ?? "",
88
+ running: props.entry.frontendRequirement?.running ?? "",
89
+ }),
90
+ interfaces: () =>
91
+ t("modules.store.requires_first", {
92
+ interfaces: props.entry.missingInterfaces
93
+ .map((missing) => missing.interfacePackage)
94
+ .join(LIST_SEPARATOR),
95
+ }),
96
+ dependents: () =>
97
+ t("modules.store.used_by", {
98
+ modules: props.entry.dependents.join(LIST_SEPARATOR),
99
+ }),
100
+ };
101
+
102
+ /** Why the main action waits, when it does. */
103
+ const blockedReason = computed(() => {
104
+ if (props.isLocked) return t("modules.store.locked");
105
+ if (!primary.value) return undefined;
106
+ const block = storeActionBlock(props.entry, primary.value.kind);
107
+ return block ? BLOCK_MESSAGES[block]() : undefined;
108
+ });
109
+ const isBlocked = computed(() => blockedReason.value !== undefined);
110
+
111
+ const operation = computed(() => props.entry.operation);
112
+ /**
113
+ * Whether the running install loads modules for missing interfaces. Kept
114
+ * for the whole operation: once they run, the listing no longer misses them.
115
+ */
116
+ const isInstallingDependencies = ref(false);
117
+ watch(
118
+ operation,
119
+ (running) => {
120
+ if (!running) {
121
+ isInstallingDependencies.value = false;
122
+ return;
123
+ }
124
+ if (
125
+ running.step === "dependencies" ||
126
+ (running.kind === "install" && props.entry.missingInterfaces.length > 0)
127
+ ) {
128
+ isInstallingDependencies.value = true;
129
+ }
130
+ },
131
+ { immediate: true },
132
+ );
133
+
134
+ const steps = computed(() => {
135
+ const kind = operation.value?.kind;
136
+ if (!kind) return [];
137
+ const current = PROGRESS_STEPS.indexOf(operation.value.step);
138
+ const kindSteps = KIND_STEPS[kind].filter(
139
+ (step) => step !== "dependencies" || isInstallingDependencies.value,
140
+ );
141
+ return kindSteps.map((step) => {
142
+ const index = PROGRESS_STEPS.indexOf(step);
143
+ const state =
144
+ index < current ? "done" : index === current ? "running" : "todo";
145
+ return { step, state };
146
+ });
147
+ });
19
148
 
20
- const command = computed(() => moduleInstallCommand(props.entry));
149
+ const STEP_ICONS: Record<string, string> = {
150
+ done: "i-ph-check-circle-fill",
151
+ running: "i-ph-circle-notch",
152
+ todo: "i-ph-circle",
153
+ };
154
+
155
+ const PLAIN_VERSION = /^\d/;
156
+
157
+ /** A version as the footer shows it: `v1.2.3`, or a range as written. */
158
+ function formatVersion(version: string): string {
159
+ return PLAIN_VERSION.test(version) ? `v${version}` : version;
160
+ }
161
+
162
+ const versionLabel = computed(() => {
163
+ const { installedVersion, latestVersion, operation: running } = props.entry;
164
+ if (running?.targetVersion && installedVersion && running.kind === "update") {
165
+ return `${formatVersion(installedVersion)} → ${formatVersion(running.targetVersion)}`;
166
+ }
167
+ if (props.entry.updateAvailable && installedVersion && latestVersion) {
168
+ return `${formatVersion(installedVersion)} → ${formatVersion(latestVersion)}`;
169
+ }
170
+ const version = installedVersion ?? latestVersion;
171
+ return version ? formatVersion(version) : undefined;
172
+ });
21
173
  </script>
22
174
 
23
175
  <template>
@@ -26,49 +178,115 @@ const command = computed(() => moduleInstallCommand(props.entry));
26
178
  :title="processI18n(props.entry.title)"
27
179
  :description="processI18n(props.entry.description)"
28
180
  :status="{
29
- tone: 'neutral',
30
- dot: 'none',
31
- label: t('modules.store.available_soon'),
181
+ tone: status.tone,
182
+ dot: status.dot,
183
+ label: t(`modules.store.state.${status.labelKey}`),
32
184
  }"
185
+ :is-solid="isInstalledHere"
33
186
  >
34
- <!-- The CLI command that adds the module today, shown muted: the store
35
- will run it for you once installing from here is possible. -->
187
+ <!-- Running operation: its steps, the current one spinning. -->
188
+ <ol
189
+ v-if="steps.length > 0"
190
+ class="flex flex-col gap-1 text-xs"
191
+ :aria-label="t(`modules.store.progress.${operation?.kind}`)"
192
+ >
193
+ <li
194
+ v-for="{ step, state } in steps"
195
+ :key="step"
196
+ class="flex items-center gap-1.5"
197
+ :class="state === 'todo' ? 'text-dimmed' : 'text-toned'"
198
+ >
199
+ <UIcon
200
+ :name="STEP_ICONS[state]"
201
+ class="size-3.5 shrink-0"
202
+ :class="{
203
+ 'animate-spin text-(--ui-primary)': state === 'running',
204
+ 'text-(--ui-success)': state === 'done',
205
+ }"
206
+ />
207
+ {{ t(`modules.store.steps.${step}`) }}
208
+ </li>
209
+ </ol>
210
+
211
+ <p
212
+ v-else-if="props.entry.state === 'error' && props.entry.error"
213
+ class="rounded-[8px] bg-(--ui-error)/8 px-2.5 py-1.5 text-xs text-(--ui-error)"
214
+ :title="props.entry.error"
215
+ >
216
+ <span class="line-clamp-3 break-words">{{ props.entry.error }}</span>
217
+ </p>
218
+
219
+ <p
220
+ v-else-if="props.entry.state === 'pending'"
221
+ class="text-xs text-(--ui-warning)"
222
+ >
223
+ {{ t("modules.store.pending_hint") }}
224
+ </p>
225
+
226
+ <!-- The npm package the module comes from. -->
36
227
  <div
37
- class="text-dimmed flex h-[30px] min-w-0 items-center gap-1.5 rounded-[8px] border border-(--ui-border) bg-(--dms-bg-muted) px-2.5 font-mono text-[11.5px]"
38
- :title="command"
228
+ v-else
229
+ class="text-dimmed flex h-[30px] min-w-0 items-center rounded-[8px] border border-(--ui-border) bg-(--dms-bg-muted) px-2.5 font-mono text-[11.5px]"
230
+ :title="props.entry.package"
39
231
  >
40
- <span class="sr-only">{{ t("modules.store.command_label") }}:</span>
41
- <!-- The package name never truncates: the CLI prefix gives way first. -->
42
- <span class="min-w-0 truncate">
43
- <span aria-hidden="true" class="opacity-70">$</span>
44
- {{ MODULE_INSTALL_COMMAND }}
45
- </span>
46
- <b class="text-toned shrink-0 font-semibold">
47
- {{ props.entry.packageName }}
48
- </b>
232
+ <span class="truncate">{{ props.entry.package }}</span>
49
233
  </div>
50
234
 
235
+ <ModuleStoreRequirements v-if="steps.length === 0" :entry="props.entry" />
236
+
51
237
  <template #footer>
52
238
  <span class="truncate">
53
239
  {{ processI18n(props.entry.catalogCategory) }}
54
240
  </span>
55
- <!-- aria-disabled rather than disabled: the button stays focusable and
56
- hoverable so its "Coming soon" tooltip can explain why. -->
57
- <span class="ms-auto shrink-0 font-sans">
241
+ <span
242
+ v-if="versionLabel"
243
+ class="shrink-0"
244
+ :class="props.entry.updateAvailable ? 'text-(--ui-warning)' : ''"
245
+ >
246
+ {{ versionLabel }}
247
+ </span>
248
+ <span class="ms-auto flex shrink-0 items-center gap-1 font-sans">
58
249
  <UTooltip
59
- :text="t('modules.store.install_soon')"
250
+ v-if="primary"
251
+ :text="blockedReason"
252
+ :disabled="!blockedReason"
60
253
  :content="TOOLTIP_CONTENT"
61
254
  >
255
+ <!-- aria-disabled rather than disabled: the button keeps its
256
+ tooltip, which says why it waits. -->
62
257
  <UButton
258
+ size="xs"
259
+ :color="primary.kind === 'update' ? 'warning' : 'neutral'"
260
+ :variant="primary.kind === 'install' ? 'solid' : 'outline'"
261
+ :icon="primary.icon"
262
+ :label="t(`modules.store.actions.${primary.labelKey}`)"
263
+ :aria-disabled="isBlocked ? 'true' : undefined"
264
+ :class="{ 'cursor-not-allowed opacity-50': isBlocked }"
265
+ @click="!isBlocked && emit('action', primary.kind)"
266
+ />
267
+ </UTooltip>
268
+ <UDropdownMenu
269
+ v-if="secondary.length > 0 && !props.isLocked"
270
+ :items="secondary"
271
+ :content="{ align: 'end' }"
272
+ >
273
+ <UButton
274
+ v-if="primary"
275
+ size="xs"
276
+ color="neutral"
277
+ variant="ghost"
278
+ icon="i-ph-dots-three-vertical"
279
+ :aria-label="t('modules.store.more_actions')"
280
+ />
281
+ <UButton
282
+ v-else
63
283
  size="xs"
64
284
  color="neutral"
65
285
  variant="outline"
66
- icon="i-ph-download-simple"
67
- :label="t('modules.store.install')"
68
- aria-disabled="true"
69
- @click.prevent
286
+ trailing-icon="i-ph-caret-down"
287
+ :label="t('modules.store.actions.manage')"
70
288
  />
71
- </UTooltip>
289
+ </UDropdownMenu>
72
290
  </span>
73
291
  </template>
74
292
  </ModuleTile>
@@ -23,6 +23,11 @@ interface ModuleTileProps {
23
23
  status: ModuleTileStatus;
24
24
  /** Where the tile leads; without it, the tile is a placeholder. */
25
25
  to?: string;
26
+ /**
27
+ * A tile with no link that still stands for a module running here (one
28
+ * the store manages): drawn solid rather than dashed.
29
+ */
30
+ isSolid?: boolean;
26
31
  }
27
32
 
28
33
  interface ModuleTileSlots {
@@ -36,6 +41,7 @@ interface ModuleTileSlots {
36
41
 
37
42
  const props = withDefaults(defineProps<ModuleTileProps>(), {
38
43
  to: undefined,
44
+ isSolid: false,
39
45
  });
40
46
  defineSlots<ModuleTileSlots>();
41
47
 
@@ -58,10 +64,18 @@ const theme = tv({
58
64
  footer: "py-[7px]",
59
65
  },
60
66
  },
67
+ isSolid: {
68
+ true: {
69
+ root: "dms-card border-solid",
70
+ footer: "bg-(--dms-bg-muted) py-[7px]",
71
+ },
72
+ },
61
73
  },
62
74
  });
63
75
 
64
- const ui = computed(() => theme({ isPlaceholder: !props.to }));
76
+ const ui = computed(() =>
77
+ theme({ isPlaceholder: !props.to, isSolid: !props.to && props.isSolid }),
78
+ );
65
79
  </script>
66
80
 
67
81
  <template>
@@ -72,7 +86,10 @@ const ui = computed(() => theme({ isPlaceholder: !props.to }));
72
86
  :class="ui.root()"
73
87
  >
74
88
  <div class="flex items-start justify-between gap-2.5">
75
- <DmsIconWell :icon="props.icon" :tone="props.to ? undefined : 'muted'" />
89
+ <DmsIconWell
90
+ :icon="props.icon"
91
+ :tone="props.to || props.isSolid ? undefined : 'muted'"
92
+ />
76
93
  <DmsStatusPill
77
94
  :tone="props.status.tone"
78
95
  :dot="props.status.dot"