@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +34 -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/implementations/dms-auth/index.js +1 -1
  18. package/dist/implementations/dms-auth/index.js.map +1 -1
  19. package/dist/routes/index.d.ts +1 -0
  20. package/dist/routes/index.js +1 -0
  21. package/dist/routes/index.js.map +1 -1
  22. package/dist/routes/store.d.ts +21 -0
  23. package/dist/routes/store.js +130 -0
  24. package/dist/routes/store.js.map +1 -0
  25. package/dist/store/activity.d.ts +2 -0
  26. package/dist/store/activity.js +12 -0
  27. package/dist/store/activity.js.map +1 -0
  28. package/dist/store/cache.d.ts +8 -0
  29. package/dist/store/cache.js +29 -0
  30. package/dist/store/cache.js.map +1 -0
  31. package/dist/store/catalog.d.ts +4 -0
  32. package/dist/store/catalog.js +69 -0
  33. package/dist/store/catalog.js.map +1 -0
  34. package/dist/store/compatibility.d.ts +3 -0
  35. package/dist/store/compatibility.js +45 -0
  36. package/dist/store/compatibility.js.map +1 -0
  37. package/dist/store/declarations.d.ts +18 -0
  38. package/dist/store/declarations.js +116 -0
  39. package/dist/store/declarations.js.map +1 -0
  40. package/dist/store/frontend-compatibility.d.ts +18 -0
  41. package/dist/store/frontend-compatibility.js +93 -0
  42. package/dist/store/frontend-compatibility.js.map +1 -0
  43. package/dist/store/interfaces.d.ts +6 -0
  44. package/dist/store/interfaces.js +126 -0
  45. package/dist/store/interfaces.js.map +1 -0
  46. package/dist/store/listing.d.ts +2 -0
  47. package/dist/store/listing.js +180 -0
  48. package/dist/store/listing.js.map +1 -0
  49. package/dist/store/operations.d.ts +8 -0
  50. package/dist/store/operations.js +254 -0
  51. package/dist/store/operations.js.map +1 -0
  52. package/dist/store/registry.d.ts +4 -0
  53. package/dist/store/registry.js +61 -0
  54. package/dist/store/registry.js.map +1 -0
  55. package/dist/store/requirements.d.ts +14 -0
  56. package/dist/store/requirements.js +81 -0
  57. package/dist/store/requirements.js.map +1 -0
  58. package/dist/store/runtime.d.ts +11 -0
  59. package/dist/store/runtime.js +78 -0
  60. package/dist/store/runtime.js.map +1 -0
  61. package/dist/store/tarball.d.ts +2 -0
  62. package/dist/store/tarball.js +80 -0
  63. package/dist/store/tarball.js.map +1 -0
  64. package/dist/store/types.d.ts +98 -0
  65. package/dist/store/types.js +3 -0
  66. package/dist/store/types.js.map +1 -0
  67. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js +1 -1
  68. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js.map +1 -1
  69. package/dist/test/unit/store/store.test.d.ts +1 -0
  70. package/dist/test/unit/store/store.test.js +127 -0
  71. package/dist/test/unit/store/store.test.js.map +1 -0
  72. package/dist/test/unit/utils/user-activity.test.js +4 -12
  73. package/dist/test/unit/utils/user-activity.test.js.map +1 -1
  74. package/dist/utils/user-activity.d.ts +1 -1
  75. package/dist/utils/user-activity.js +6 -3
  76. package/dist/utils/user-activity.js.map +1 -1
  77. package/dist/validation/store.schema.d.ts +39 -0
  78. package/dist/validation/store.schema.js +27 -0
  79. package/dist/validation/store.schema.js.map +1 -0
  80. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreDependencyPicker.vue +136 -0
  81. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +98 -0
  82. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +247 -29
  83. package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +19 -2
  84. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +186 -120
  85. package/frontend-vue/layers/dms-layout/app/build/utils/module-store-install.ts +146 -0
  86. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +109 -0
  87. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +335 -34
  88. package/frontend-vue/layers/dms-layout/app/types/module-store.ts +134 -0
  89. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +107 -9
  90. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +107 -9
  91. package/frontend-vue/package.json +3 -3
  92. package/frontend-vue/pnpm-lock.yaml +16 -22
  93. package/frontend-vue/tests/module-store-catalog.test.ts +270 -51
  94. package/frontend-vue/tests/top-list-card-period-badge-ssr.test.ts +212 -0
  95. package/package.json +5 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.schema.js","sourceRoot":"","sources":["../../src/validation/store.schema.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAClD,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAGpB,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;CACpE,CAAC,CAAC;AAMH,MAAM,mBAAmB,GACvB,sDAAsD,CAAC;AACzD,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,MAAM,gBAAgB,GAAG,OAAC;KACvB,MAAM,EAAE;KACR,GAAG,CAAC,sBAAsB,CAAC;KAC3B,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAMjB,QAAA,kBAAkB,GAAG,4BAAoB,CAAC,MAAM,CAAC;IAC5D,YAAY,EAAE,OAAC;SACZ,KAAK,CACJ,OAAC,CAAC,MAAM,CAAC;QACP,gBAAgB,EAAE,gBAAgB;QAClC,OAAO,EAAE,gBAAgB;QACzB,MAAM,EAAE,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;KACzC,CAAC,CACH;SACA,GAAG,CAAC,gBAAgB,CAAC;SACrB,QAAQ,EAAE;CACd,CAAC,CAAC"}
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import { MONO_CHIP_CLASS } from "#dms-ui/app/build/utils/monoChip";
3
+ import type { MissingInterface } from "../../../../types/module-store";
4
+ import {
5
+ type DependencyDraft,
6
+ type DependencyDraftError,
7
+ OTHER_PACKAGE_CHOICE,
8
+ validateDependencyDraft,
9
+ } from "../../../utils/module-store-install";
10
+
11
+ interface ModuleStoreDependencyPickerProps {
12
+ /** The interfaces the release needs and no running module implements. */
13
+ missing: MissingInterface[];
14
+ /** One draft per missing interface, in the same order. */
15
+ drafts: DependencyDraft[];
16
+ /** Confirm was tried: the fields say what is wrong. */
17
+ showErrors?: boolean;
18
+ }
19
+
20
+ interface ModuleStoreDependencyPickerEmits {
21
+ update: [index: number, patch: Partial<DependencyDraft>];
22
+ }
23
+
24
+ const props = withDefaults(defineProps<ModuleStoreDependencyPickerProps>(), {
25
+ showErrors: false,
26
+ });
27
+ const emit = defineEmits<ModuleStoreDependencyPickerEmits>();
28
+ const { t } = useI18n();
29
+
30
+ const CONFIG_PLACEHOLDER = "{}";
31
+ const CONFIG_ROWS = 3;
32
+
33
+ const groups = computed(() =>
34
+ props.missing.map((missing, index) => {
35
+ const draft = props.drafts[index];
36
+ const errors =
37
+ props.showErrors && draft ? validateDependencyDraft(draft) : {};
38
+ return {
39
+ missing,
40
+ draft,
41
+ errors,
42
+ items: [
43
+ ...missing.candidates.map((candidate) => ({
44
+ label: candidate.name,
45
+ description: candidate.package,
46
+ value: candidate.package,
47
+ })),
48
+ {
49
+ label: t("modules.store.install_dialog.other"),
50
+ value: OTHER_PACKAGE_CHOICE,
51
+ },
52
+ ],
53
+ };
54
+ }),
55
+ );
56
+
57
+ function errorText(error: DependencyDraftError | undefined) {
58
+ return error ? t(`modules.store.install_dialog.errors.${error}`) : undefined;
59
+ }
60
+
61
+ /** Sends `field` of draft `index` as the control typed it. */
62
+ function updateField(
63
+ index: number,
64
+ field: keyof DependencyDraft,
65
+ value: unknown,
66
+ ): void {
67
+ emit("update", index, { [field]: String(value ?? "") });
68
+ }
69
+
70
+ function fieldId(index: number, field: string): string {
71
+ return `store-dependency-${index}-${field}`;
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <div class="flex flex-col gap-4">
77
+ <section
78
+ v-for="(group, index) in groups"
79
+ :key="group.missing.interfacePackage"
80
+ class="flex flex-col gap-2.5"
81
+ :aria-labelledby="fieldId(index, 'title')"
82
+ >
83
+ <h4
84
+ :id="fieldId(index, 'title')"
85
+ class="text-highlighted text-[13px] font-semibold"
86
+ >
87
+ <code :class="[MONO_CHIP_CLASS, 'bg-elevated text-xs']">
88
+ {{ group.missing.interfacePackage }}
89
+ </code>
90
+ </h4>
91
+ <template v-if="group.draft">
92
+ <URadioGroup
93
+ :model-value="group.draft.choice"
94
+ :items="group.items"
95
+ size="sm"
96
+ :aria-labelledby="fieldId(index, 'title')"
97
+ @update:model-value="updateField(index, 'choice', $event)"
98
+ />
99
+ <UFormField
100
+ v-if="group.draft.choice === OTHER_PACKAGE_CHOICE"
101
+ :label="t('modules.store.install_dialog.package_label')"
102
+ :error="errorText(group.errors.package)"
103
+ >
104
+ <UInput
105
+ :id="fieldId(index, 'package')"
106
+ :model-value="group.draft.otherPackage"
107
+ class="w-full font-mono"
108
+ autocomplete="off"
109
+ spellcheck="false"
110
+ :placeholder="t('modules.store.install_dialog.package_placeholder')"
111
+ @update:model-value="updateField(index, 'otherPackage', $event)"
112
+ />
113
+ </UFormField>
114
+ <UFormField
115
+ :label="t('modules.store.install_dialog.config_label')"
116
+ :hint="t('modules.store.install_dialog.config_hint')"
117
+ :error="errorText(group.errors.config)"
118
+ >
119
+ <UTextarea
120
+ :id="fieldId(index, 'config')"
121
+ :model-value="group.draft.config"
122
+ class="w-full font-mono text-xs"
123
+ :rows="CONFIG_ROWS"
124
+ autoresize
125
+ spellcheck="false"
126
+ :placeholder="CONFIG_PLACEHOLDER"
127
+ @update:model-value="updateField(index, 'config', $event)"
128
+ />
129
+ </UFormField>
130
+ </template>
131
+ </section>
132
+ <p class="text-muted text-xs">
133
+ {{ t("modules.store.install_dialog.note") }}
134
+ </p>
135
+ </div>
136
+ </template>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import DmsCopyButton from "#dms-ui/app/components/copy/CopyButton.vue";
3
+ import type { StoreListingEntry } from "../../../../types/module-store";
4
+
5
+ interface ModuleStoreRequirementsProps {
6
+ /** The store module whose requirements the tile spells out. */
7
+ entry: StoreListingEntry;
8
+ }
9
+
10
+ const props = defineProps<ModuleStoreRequirementsProps>();
11
+ const { t } = useI18n();
12
+
13
+ const LIST_SEPARATOR = ", ";
14
+
15
+ const requirement = computed(() => props.entry.frontendRequirement);
16
+ const missingInterfaces = computed(() =>
17
+ props.entry.missingInterfaces
18
+ .map((missing) => missing.interfacePackage)
19
+ .join(LIST_SEPARATOR),
20
+ );
21
+ const dependents = computed(() => props.entry.dependents.join(LIST_SEPARATOR));
22
+ </script>
23
+
24
+ <template>
25
+ <!-- The release needs another dms-frontend: what to run, what it breaks.
26
+ The store never moves dms-frontend itself. -->
27
+ <div
28
+ v-if="requirement"
29
+ class="flex flex-col gap-1.5 rounded-[8px] bg-(--ui-warning)/8 px-2.5 py-1.5 text-xs text-(--ui-warning)"
30
+ >
31
+ <p>
32
+ {{
33
+ t("modules.store.frontend_requirement.needs", {
34
+ range: requirement.range,
35
+ running: requirement.running,
36
+ })
37
+ }}
38
+ </p>
39
+ <template v-if="requirement.target">
40
+ <p v-if="requirement.command" class="text-toned">
41
+ {{
42
+ t("modules.store.frontend_requirement.run", {
43
+ target: requirement.target,
44
+ })
45
+ }}
46
+ </p>
47
+ <div
48
+ v-if="requirement.command"
49
+ class="text-highlighted flex min-w-0 items-center gap-1 rounded-[6px] border border-(--ui-border) bg-(--dms-bg-muted) ps-2 font-mono text-[11px]"
50
+ >
51
+ <code class="min-w-0 flex-1 truncate" :title="requirement.command">
52
+ {{ requirement.command }}
53
+ </code>
54
+ <DmsCopyButton :value="requirement.command" />
55
+ </div>
56
+ <template v-if="requirement.breaks.length > 0">
57
+ <p>
58
+ {{
59
+ t("modules.store.frontend_requirement.breaks", {
60
+ target: requirement.target,
61
+ })
62
+ }}
63
+ </p>
64
+ <ul class="text-toned flex flex-col gap-0.5">
65
+ <li
66
+ v-for="layer in requirement.breaks"
67
+ :key="layer.name"
68
+ class="flex min-w-0 items-baseline gap-1.5"
69
+ >
70
+ <span class="truncate font-mono">{{ layer.name }}</span>
71
+ <span class="text-dimmed shrink-0">
72
+ {{
73
+ t("modules.store.frontend_requirement.supports", {
74
+ range: layer.range,
75
+ })
76
+ }}
77
+ </span>
78
+ </li>
79
+ </ul>
80
+ </template>
81
+ </template>
82
+ <p v-else class="text-toned">
83
+ {{ t("modules.store.frontend_requirement.no_target") }}
84
+ </p>
85
+ </div>
86
+
87
+ <p
88
+ v-if="missingInterfaces"
89
+ class="text-toned truncate text-xs"
90
+ :title="missingInterfaces"
91
+ >
92
+ {{ t("modules.store.requires", { interfaces: missingInterfaces }) }}
93
+ </p>
94
+
95
+ <p v-if="dependents" class="text-xs text-(--ui-warning)">
96
+ {{ t("modules.store.used_by", { modules: dependents }) }}
97
+ </p>
98
+ </template>
@@ -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"