@abraca/nuxt 2.0.11 → 2.4.0
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/dist/module.d.mts +68 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +99 -4
- package/dist/runtime/components/ACodeEditor.d.vue.ts +26 -0
- package/dist/runtime/components/ACodeEditor.vue +268 -0
- package/dist/runtime/components/ACodeEditor.vue.d.ts +26 -0
- package/dist/runtime/components/ADocumentTree.vue +52 -20
- package/dist/runtime/components/AEditor.d.vue.ts +20 -13
- package/dist/runtime/components/AEditor.vue +55 -2
- package/dist/runtime/components/AEditor.vue.d.ts +20 -13
- package/dist/runtime/components/ANodePanel.vue +64 -60
- package/dist/runtime/components/ANotificationBell.d.vue.ts +1 -1
- package/dist/runtime/components/ANotificationBell.vue.d.ts +1 -1
- package/dist/runtime/components/ASpaceFormModal.d.vue.ts +2 -2
- package/dist/runtime/components/ASpaceFormModal.vue.d.ts +2 -2
- package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
- package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
- package/dist/runtime/components/aware/APresenceBlobs.d.vue.ts +29 -1
- package/dist/runtime/components/aware/APresenceBlobs.vue +54 -8
- package/dist/runtime/components/aware/APresenceBlobs.vue.d.ts +29 -1
- package/dist/runtime/components/aware/APresenceCursors.d.vue.ts +11 -0
- package/dist/runtime/components/aware/APresenceCursors.vue +74 -9
- package/dist/runtime/components/aware/APresenceCursors.vue.d.ts +11 -0
- package/dist/runtime/components/aware/AToggleGroup.d.vue.ts +28 -13
- package/dist/runtime/components/aware/AToggleGroup.vue +56 -20
- package/dist/runtime/components/aware/AToggleGroup.vue.d.ts +28 -13
- package/dist/runtime/components/docs/ADocsNavigation.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsNavigation.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsToc.d.vue.ts +2 -2
- package/dist/runtime/components/docs/ADocsToc.vue.d.ts +2 -2
- package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +1 -1
- package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AFileGlbViewer.vue +27 -10
- package/dist/runtime/components/editor/ANodeInlineLabel.d.vue.ts +1 -1
- package/dist/runtime/components/editor/ANodeInlineLabel.vue.d.ts +1 -1
- package/dist/runtime/components/registry/APluginBrowser.d.vue.ts +23 -0
- package/dist/runtime/components/registry/APluginBrowser.vue +155 -0
- package/dist/runtime/components/registry/APluginBrowser.vue.d.ts +23 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.d.vue.ts +17 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue +159 -0
- package/dist/runtime/components/registry/APluginCapabilityDialog.vue.d.ts +17 -0
- package/dist/runtime/components/registry/APluginCard.d.vue.ts +20 -0
- package/dist/runtime/components/registry/APluginCard.vue +91 -0
- package/dist/runtime/components/registry/APluginCard.vue.d.ts +20 -0
- package/dist/runtime/components/registry/APluginDetail.d.vue.ts +18 -0
- package/dist/runtime/components/registry/APluginDetail.vue +252 -0
- package/dist/runtime/components/registry/APluginDetail.vue.d.ts +18 -0
- package/dist/runtime/components/renderers/ACodeRenderer.d.vue.ts +15 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue +68 -0
- package/dist/runtime/components/renderers/ACodeRenderer.vue.d.ts +15 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue +416 -120
- package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
- package/dist/runtime/components/renderers/sheets/ASheetsToolbar.d.vue.ts +4 -4
- package/dist/runtime/components/renderers/sheets/ASheetsToolbar.vue.d.ts +4 -4
- package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +11 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +16 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +11 -0
- package/dist/runtime/components/shell/ASettingsSection.d.vue.ts +35 -0
- package/dist/runtime/components/shell/ASettingsSection.vue +26 -0
- package/dist/runtime/components/shell/ASettingsSection.vue.d.ts +35 -0
- package/dist/runtime/components/shell/ASidebar.d.vue.ts +1 -1
- package/dist/runtime/components/shell/ASidebar.vue.d.ts +1 -1
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +3 -0
- package/dist/runtime/components/shell/AUserMenu.vue +4 -0
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +3 -0
- package/dist/runtime/composables/useAbracadabraSchema.d.ts +83 -0
- package/dist/runtime/composables/useAbracadabraSchema.js +52 -0
- package/dist/runtime/composables/useAggregatedPresence.d.ts +1 -6
- package/dist/runtime/composables/useCalendarView.d.ts +1 -1
- package/dist/runtime/composables/useChat.js +1 -0
- package/dist/runtime/composables/useDocBreadcrumb.d.ts +21 -0
- package/dist/runtime/composables/useDocBreadcrumb.js +33 -0
- package/dist/runtime/composables/useDocEntryTyped.d.ts +60 -0
- package/dist/runtime/composables/useDocEntryTyped.js +70 -0
- package/dist/runtime/composables/useEditorDragHandle.js +18 -0
- package/dist/runtime/composables/useEditorSuggestions.js +2 -1
- package/dist/runtime/composables/useInstalledPlugins.d.ts +3 -21
- package/dist/runtime/composables/useInstalledPlugins.js +2 -12
- package/dist/runtime/composables/useMetaMenuItems.d.ts +21 -0
- package/dist/runtime/composables/useMetaMenuItems.js +115 -0
- package/dist/runtime/composables/useMetaValidator.d.ts +27 -0
- package/dist/runtime/composables/useMetaValidator.js +10 -0
- package/dist/runtime/composables/usePluginCatalog.d.ts +161 -0
- package/dist/runtime/composables/usePluginCatalog.js +234 -0
- package/dist/runtime/composables/useQuery.d.ts +79 -0
- package/dist/runtime/composables/useQuery.js +97 -0
- package/dist/runtime/composables/useSpaces.js +4 -5
- package/dist/runtime/composables/useTableView.d.ts +3 -3
- package/dist/runtime/composables/useTypedDoc.d.ts +97 -0
- package/dist/runtime/composables/useTypedDoc.js +114 -0
- package/dist/runtime/composables/useWebRTC.js +44 -5
- package/dist/runtime/extensions/document-meta.js +5 -0
- package/dist/runtime/extensions/timeline.d.ts +11 -0
- package/dist/runtime/extensions/timeline.js +52 -0
- package/dist/runtime/extensions/views/DocumentMetaView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue +63 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue +131 -0
- package/dist/runtime/extensions/views/TimelineItemView.vue.d.ts +4 -0
- package/dist/runtime/extensions/views/TimelineView.d.vue.ts +9 -0
- package/dist/runtime/extensions/views/TimelineView.vue +29 -0
- package/dist/runtime/extensions/views/TimelineView.vue.d.ts +9 -0
- package/dist/runtime/locale.d.ts +2 -0
- package/dist/runtime/locale.js +2 -0
- package/dist/runtime/plugin-abracadabra.client.js +107 -6
- package/dist/runtime/plugin-registry.d.ts +11 -30
- package/dist/runtime/plugin-registry.js +2 -82
- package/dist/runtime/plugins/core.plugin.js +10 -4
- package/dist/runtime/server/api/_abracadabra/spaces.get.d.ts +1 -1
- package/dist/runtime/server/plugins/abracadabra-service.js +28 -0
- package/dist/runtime/server/utils/docCache.js +24 -3
- package/dist/runtime/server/utils/schemaServerSupport.d.ts +52 -0
- package/dist/runtime/server/utils/schemaServerSupport.js +51 -0
- package/dist/runtime/types.d.ts +63 -46
- package/dist/runtime/utils/docTypes.d.ts +15 -0
- package/dist/runtime/utils/docTypes.js +20 -0
- package/dist/runtime/utils/loadCodeMirror.d.ts +32 -0
- package/dist/runtime/utils/loadCodeMirror.js +65 -0
- package/dist/runtime/utils/loadThree.d.ts +18 -0
- package/dist/runtime/utils/loadThree.js +46 -0
- package/dist/runtime/utils/markdownToYjs.d.ts +1 -23
- package/dist/runtime/utils/markdownToYjs.js +5 -440
- package/dist/runtime/utils/schemaSupport.d.ts +60 -0
- package/dist/runtime/utils/schemaSupport.js +40 -0
- package/dist/runtime/utils/yjsConvert.d.ts +1 -14
- package/dist/runtime/utils/yjsConvert.js +5 -331
- package/package.json +86 -21
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { ref, computed, shallowRef } from "vue";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { useInstalledPlugins } from "./useInstalledPlugins.js";
|
|
4
|
+
export function usePluginCatalog() {
|
|
5
|
+
const cfg = useRuntimeConfig();
|
|
6
|
+
const base = cfg.public.abracadabra?.pluginRegistry?.url || "http://127.0.0.1:8787";
|
|
7
|
+
const plugins = shallowRef([]);
|
|
8
|
+
const isLoading = ref(false);
|
|
9
|
+
const error = ref(null);
|
|
10
|
+
const pagination = ref(null);
|
|
11
|
+
const details = shallowRef({});
|
|
12
|
+
const policy = ref(null);
|
|
13
|
+
async function list(params = {}) {
|
|
14
|
+
isLoading.value = true;
|
|
15
|
+
error.value = null;
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(`${base.replace(/\/$/, "")}/v1/plugins`);
|
|
18
|
+
if (params.category) url.searchParams.set("category", params.category);
|
|
19
|
+
if (params.search) url.searchParams.set("search", params.search);
|
|
20
|
+
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
|
|
21
|
+
if (typeof params.offset === "number") url.searchParams.set("offset", String(params.offset));
|
|
22
|
+
const result = await $fetch(url.toString());
|
|
23
|
+
plugins.value = result.plugins;
|
|
24
|
+
pagination.value = result.pagination;
|
|
25
|
+
return result;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
28
|
+
throw error.value;
|
|
29
|
+
} finally {
|
|
30
|
+
isLoading.value = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function get(id) {
|
|
34
|
+
if (details.value[id]) return details.value[id];
|
|
35
|
+
const detail = await $fetch(`${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}`);
|
|
36
|
+
details.value = { ...details.value, [id]: detail };
|
|
37
|
+
return detail;
|
|
38
|
+
}
|
|
39
|
+
async function getLatest(id) {
|
|
40
|
+
return await $fetch(
|
|
41
|
+
`${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/latest`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
async function getVersion(id, version) {
|
|
45
|
+
return await $fetch(
|
|
46
|
+
`${base.replace(/\/$/, "")}/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const installed = useInstalledPlugins();
|
|
50
|
+
function isInstalled(id) {
|
|
51
|
+
return installed.entries.value.some((e) => e.name === id || e.url.includes(`/${id}/`));
|
|
52
|
+
}
|
|
53
|
+
async function install(id) {
|
|
54
|
+
const decision = policyDecisionFor(id);
|
|
55
|
+
if (decision.state === "blocked") {
|
|
56
|
+
throw new Error(`install refused by server policy: ${decision.reason}`);
|
|
57
|
+
}
|
|
58
|
+
const latest = await getLatest(id);
|
|
59
|
+
if (!latest.artifact_url) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`plugin "${id}" version ${latest.version} has no artifact URL \u2014 registry artifact storage not configured yet`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
installed.install(latest.artifact_url);
|
|
65
|
+
installed.updateMeta(latest.artifact_url, {
|
|
66
|
+
name: id,
|
|
67
|
+
version: latest.version,
|
|
68
|
+
label: latest.manifest.name ?? id,
|
|
69
|
+
description: latest.manifest.description
|
|
70
|
+
});
|
|
71
|
+
return latest;
|
|
72
|
+
}
|
|
73
|
+
async function installFromUrl(url) {
|
|
74
|
+
const p = policy.value;
|
|
75
|
+
if (p && !p.allow_unsafe_install) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"install refused by server policy: out-of-registry URL/file installs are disabled"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
installed.install(url);
|
|
81
|
+
}
|
|
82
|
+
async function checkUpdates() {
|
|
83
|
+
const p = policy.value;
|
|
84
|
+
if (p && p.auto_update === "off") return [];
|
|
85
|
+
const outcomes = [];
|
|
86
|
+
for (const entry of installed.entries.value) {
|
|
87
|
+
if (!entry.name || !entry.version) continue;
|
|
88
|
+
let latest;
|
|
89
|
+
try {
|
|
90
|
+
latest = await getLatest(entry.name);
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const decision = policyDecisionFor(entry.name);
|
|
95
|
+
if (decision.state === "blocked") {
|
|
96
|
+
outcomes.push({
|
|
97
|
+
pluginId: entry.name,
|
|
98
|
+
fromVersion: entry.version,
|
|
99
|
+
toVersion: latest.version,
|
|
100
|
+
state: "skipped-blocked",
|
|
101
|
+
newCapabilities: [],
|
|
102
|
+
reason: decision.reason
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const bump = semverBump(entry.version, latest.version);
|
|
107
|
+
if (!bump) continue;
|
|
108
|
+
if (!bumpAllowed(bump, p?.auto_update ?? "patch")) {
|
|
109
|
+
outcomes.push({
|
|
110
|
+
pluginId: entry.name,
|
|
111
|
+
fromVersion: entry.version,
|
|
112
|
+
toVersion: latest.version,
|
|
113
|
+
state: "skipped-policy",
|
|
114
|
+
newCapabilities: [],
|
|
115
|
+
reason: `policy is '${p?.auto_update ?? "patch"}', would not apply '${bump}' bump`
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let installedCaps = [];
|
|
120
|
+
try {
|
|
121
|
+
const installedDetail = await getVersion(entry.name, entry.version);
|
|
122
|
+
installedCaps = installedDetail.manifest.capabilities.required ?? [];
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
const latestCaps = latest.manifest.capabilities.required ?? [];
|
|
126
|
+
const newCaps = latestCaps.filter((c) => !installedCaps.includes(c));
|
|
127
|
+
const requireReview = p?.require_review_on_cap_growth ?? true;
|
|
128
|
+
if (newCaps.length > 0 && requireReview) {
|
|
129
|
+
outcomes.push({
|
|
130
|
+
pluginId: entry.name,
|
|
131
|
+
fromVersion: entry.version,
|
|
132
|
+
toVersion: latest.version,
|
|
133
|
+
state: "pending-review",
|
|
134
|
+
newCapabilities: newCaps,
|
|
135
|
+
reason: "new capabilities requested"
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (latest.artifact_url) {
|
|
140
|
+
installed.uninstall(entry.url);
|
|
141
|
+
installed.install(latest.artifact_url);
|
|
142
|
+
installed.updateMeta(latest.artifact_url, {
|
|
143
|
+
name: entry.name,
|
|
144
|
+
version: latest.version,
|
|
145
|
+
label: latest.manifest.name ?? entry.name,
|
|
146
|
+
description: latest.manifest.description
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
outcomes.push({
|
|
150
|
+
pluginId: entry.name,
|
|
151
|
+
fromVersion: entry.version,
|
|
152
|
+
toVersion: latest.version,
|
|
153
|
+
state: "applied",
|
|
154
|
+
newCapabilities: newCaps
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return outcomes;
|
|
158
|
+
}
|
|
159
|
+
function capabilitiesFor(detail) {
|
|
160
|
+
const m = "manifest" in detail ? detail.manifest : detail;
|
|
161
|
+
return {
|
|
162
|
+
required: [...m.capabilities.required ?? []],
|
|
163
|
+
optional: [...m.capabilities.optional ?? []]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function loadPolicy(serverUrl) {
|
|
167
|
+
const url = `${serverUrl.replace(/\/$/, "")}/plugins/policy`;
|
|
168
|
+
const fetched = await $fetch(url);
|
|
169
|
+
policy.value = fetched;
|
|
170
|
+
return fetched;
|
|
171
|
+
}
|
|
172
|
+
function policyDecisionFor(pluginId) {
|
|
173
|
+
const p = policy.value;
|
|
174
|
+
if (!p) return { state: "unknown" };
|
|
175
|
+
if (p.allowlist.includes(pluginId)) return { state: "allowed" };
|
|
176
|
+
if (p.allow_user_install) return { state: "gated" };
|
|
177
|
+
return {
|
|
178
|
+
state: "blocked",
|
|
179
|
+
reason: "Your server only permits plugins from its allowlist."
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const categories = computed(() => {
|
|
183
|
+
const set = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const p of plugins.value) for (const c of p.categories) set.add(c);
|
|
185
|
+
return [...set].sort();
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
/** Public catalog (latest page fetched). Refresh via `list()`. */
|
|
189
|
+
plugins,
|
|
190
|
+
/** Distinct category tags across `plugins`, alphabetical. */
|
|
191
|
+
categories,
|
|
192
|
+
pagination,
|
|
193
|
+
isLoading,
|
|
194
|
+
error,
|
|
195
|
+
list,
|
|
196
|
+
get,
|
|
197
|
+
getLatest,
|
|
198
|
+
getVersion,
|
|
199
|
+
install,
|
|
200
|
+
installFromUrl,
|
|
201
|
+
isInstalled,
|
|
202
|
+
capabilitiesFor,
|
|
203
|
+
/** Current server policy. `null` until `loadPolicy()` resolves. */
|
|
204
|
+
policy,
|
|
205
|
+
loadPolicy,
|
|
206
|
+
/** Per-plugin install gate. Drives every Install button across the UI. */
|
|
207
|
+
policyDecisionFor,
|
|
208
|
+
checkUpdates
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function semverBump(from, to) {
|
|
212
|
+
const a = parseSemver(from);
|
|
213
|
+
const b = parseSemver(to);
|
|
214
|
+
if (!a || !b) return null;
|
|
215
|
+
if (b.major > a.major) return "major";
|
|
216
|
+
if (b.major < a.major) return null;
|
|
217
|
+
if (b.minor > a.minor) return "minor";
|
|
218
|
+
if (b.minor < a.minor) return null;
|
|
219
|
+
if (b.patch > a.patch) return "patch";
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function parseSemver(v) {
|
|
223
|
+
const cleaned = v.replace(/^v/, "").split(/[-+]/)[0];
|
|
224
|
+
if (!cleaned) return null;
|
|
225
|
+
const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10));
|
|
226
|
+
if (parts.length !== 3 || parts.some((p) => Number.isNaN(p))) return null;
|
|
227
|
+
return { major: parts[0], minor: parts[1], patch: parts[2] };
|
|
228
|
+
}
|
|
229
|
+
function bumpAllowed(bump, policy) {
|
|
230
|
+
if (policy === "off") return false;
|
|
231
|
+
if (policy === "patch") return bump === "patch";
|
|
232
|
+
if (policy === "minor") return bump === "patch" || bump === "minor";
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useQuery — query + live-subscribe to documents in the doc-tree.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Imperative (`subscribe: false`, default)** — thin Vue-reactive
|
|
7
|
+
* shell around `client.queryDocs`. Call `refresh()` to re-fetch.
|
|
8
|
+
* Use for "give me the matching docs right now" reads.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Reactive (`subscribe: true`)** — opens a `provider.subscribeQuery`
|
|
11
|
+
* under the hood. The first frame fills `entries`; subsequent
|
|
12
|
+
* `delta` frames merge in via add/remove. Resubscribes when the
|
|
13
|
+
* options ref changes; cancels on unmount.
|
|
14
|
+
*
|
|
15
|
+
* Reactive mode requires an active provider — `entries` stays empty
|
|
16
|
+
* until the provider connects and the initial snapshot arrives.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
*
|
|
20
|
+
* // Imperative
|
|
21
|
+
* const { entries, loading, refresh } = useQuery({ type: 'kanban' })
|
|
22
|
+
* await refresh()
|
|
23
|
+
*
|
|
24
|
+
* // Reactive (mount → snapshot → live deltas → unmount cancels)
|
|
25
|
+
* const { entries } = useQuery(
|
|
26
|
+
* computed(() => ({ type: 'kanban', where: { priority: { gte: 3 } } })),
|
|
27
|
+
* { immediate: true, subscribe: true },
|
|
28
|
+
* )
|
|
29
|
+
*/
|
|
30
|
+
import { type Ref } from 'vue';
|
|
31
|
+
import type { DocumentMeta } from '@abraca/dabra';
|
|
32
|
+
export interface QueryOptions {
|
|
33
|
+
/** Match `documents.kind` exactly. */
|
|
34
|
+
type?: string;
|
|
35
|
+
/** Narrow to children of this doc. */
|
|
36
|
+
parentId?: string;
|
|
37
|
+
/** Case-insensitive substring match on `documents.label`. */
|
|
38
|
+
labelContains?: string;
|
|
39
|
+
/** Maximum number of results (default 50, hard cap 500 server-side). */
|
|
40
|
+
limit?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Predicate AST. Shape mirrors `@abraca/schema`'s `WhereClause`:
|
|
43
|
+
* `{ priority: { gte: 3 }, tags: { contains: "urgent" } }`
|
|
44
|
+
* Empty / missing falls through to the v1 filter set.
|
|
45
|
+
*/
|
|
46
|
+
where?: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface UseQueryReturn {
|
|
49
|
+
/** Latest result set. Empty until `refresh()` runs at least once. */
|
|
50
|
+
readonly entries: Ref<readonly DocumentMeta[]>;
|
|
51
|
+
/** True while a fetch is in flight. */
|
|
52
|
+
readonly loading: Ref<boolean>;
|
|
53
|
+
/** Last error message, or null. Cleared on the next refresh. */
|
|
54
|
+
readonly error: Ref<string | null>;
|
|
55
|
+
/** Trigger a fresh fetch with the current options. */
|
|
56
|
+
refresh: () => Promise<DocumentMeta[]>;
|
|
57
|
+
}
|
|
58
|
+
export interface UseQueryHookOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Run `refresh()` immediately when the composable mounts, and whenever
|
|
61
|
+
* the options ref changes. Default false — the caller invokes `refresh`
|
|
62
|
+
* explicitly. Note that when the options arg is a plain object (not a
|
|
63
|
+
* ref / computed), the watcher fires once at mount and then never
|
|
64
|
+
* again — change-tracking requires a reactive source.
|
|
65
|
+
*/
|
|
66
|
+
immediate?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Open a live `query:v1:` subscription on the provider's WebSocket
|
|
69
|
+
* instead of polling REST. The first `ack` frame fills `entries`;
|
|
70
|
+
* subsequent `delta` frames merge in. Cancels on unmount, and on
|
|
71
|
+
* options change (a fresh sub is opened with the new predicate).
|
|
72
|
+
*
|
|
73
|
+
* Implies `immediate: true` — subscriptions are useless without an
|
|
74
|
+
* initial snapshot. Falls back to imperative mode when no provider
|
|
75
|
+
* is bound or the WS is closed.
|
|
76
|
+
*/
|
|
77
|
+
subscribe?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export declare function useQuery(options: QueryOptions | Ref<QueryOptions>, hookOpts?: UseQueryHookOptions): UseQueryReturn;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
2
|
+
import { useAbracadabra } from "#imports";
|
|
3
|
+
function asOptions(input) {
|
|
4
|
+
if (input.value !== void 0 && typeof input.effect !== "undefined") {
|
|
5
|
+
return input;
|
|
6
|
+
}
|
|
7
|
+
if (input.value !== void 0) {
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
return ref(input);
|
|
11
|
+
}
|
|
12
|
+
export function useQuery(options, hookOpts = {}) {
|
|
13
|
+
const opts = asOptions(options);
|
|
14
|
+
const entries = shallowRef([]);
|
|
15
|
+
const loading = ref(false);
|
|
16
|
+
const error = ref(null);
|
|
17
|
+
async function refresh() {
|
|
18
|
+
const abra = useAbracadabra();
|
|
19
|
+
const client = abra.client?.value;
|
|
20
|
+
if (!client) {
|
|
21
|
+
entries.value = [];
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
loading.value = true;
|
|
25
|
+
error.value = null;
|
|
26
|
+
try {
|
|
27
|
+
const result = await client.queryDocs(opts.value);
|
|
28
|
+
entries.value = Object.freeze(result.slice());
|
|
29
|
+
return result;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error.value = err instanceof Error ? err.message : String(err);
|
|
32
|
+
entries.value = [];
|
|
33
|
+
return [];
|
|
34
|
+
} finally {
|
|
35
|
+
loading.value = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let activeHandle = null;
|
|
39
|
+
function closeSubscription() {
|
|
40
|
+
if (activeHandle) {
|
|
41
|
+
activeHandle.cancel();
|
|
42
|
+
activeHandle = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function specFromOpts(o) {
|
|
46
|
+
return {
|
|
47
|
+
type: o.type,
|
|
48
|
+
parentId: o.parentId,
|
|
49
|
+
labelContains: o.labelContains,
|
|
50
|
+
limit: o.limit,
|
|
51
|
+
where: o.where
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function openSubscription() {
|
|
55
|
+
closeSubscription();
|
|
56
|
+
const abra = useAbracadabra();
|
|
57
|
+
const provider = abra.provider?.value;
|
|
58
|
+
if (!provider?.subscribeQuery) {
|
|
59
|
+
await refresh();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
loading.value = true;
|
|
63
|
+
error.value = null;
|
|
64
|
+
activeHandle = provider.subscribeQuery(specFromOpts(opts.value), {
|
|
65
|
+
onSnapshot: (rows) => {
|
|
66
|
+
entries.value = Object.freeze(rows.slice());
|
|
67
|
+
loading.value = false;
|
|
68
|
+
},
|
|
69
|
+
onDelta: ({ added, removed }) => {
|
|
70
|
+
const removedSet = new Set(removed);
|
|
71
|
+
const kept = entries.value.filter(
|
|
72
|
+
(e) => !removedSet.has(e.id)
|
|
73
|
+
);
|
|
74
|
+
const next = [...added, ...kept];
|
|
75
|
+
entries.value = Object.freeze(next);
|
|
76
|
+
},
|
|
77
|
+
onError: (err) => {
|
|
78
|
+
error.value = err.message;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (hookOpts.subscribe) {
|
|
83
|
+
watch(
|
|
84
|
+
opts,
|
|
85
|
+
() => {
|
|
86
|
+
void openSubscription();
|
|
87
|
+
},
|
|
88
|
+
{ immediate: true, deep: true }
|
|
89
|
+
);
|
|
90
|
+
onScopeDispose(() => closeSubscription());
|
|
91
|
+
} else if (hookOpts.immediate) {
|
|
92
|
+
watch(opts, () => {
|
|
93
|
+
void refresh();
|
|
94
|
+
}, { immediate: true, deep: true });
|
|
95
|
+
}
|
|
96
|
+
return { entries, loading, error, refresh };
|
|
97
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ref, computed } from "vue";
|
|
2
|
-
import { useRoute } from "#imports";
|
|
3
2
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
4
3
|
const loading = ref(false);
|
|
5
4
|
const error = ref(null);
|
|
@@ -7,10 +6,10 @@ export function useSpaces() {
|
|
|
7
6
|
const abra = useAbracadabra();
|
|
8
7
|
const spaces = computed(() => abra.currentServerSpaces.value ?? []);
|
|
9
8
|
const currentSpace = computed(() => {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
13
|
-
return spaces.value.find((s) => s.
|
|
9
|
+
const entry = abra.savedServers?.value?.find?.((s) => s.url === abra.currentServerUrl?.value);
|
|
10
|
+
const entryDocId = entry?.entryDocId;
|
|
11
|
+
if (!entryDocId) return spaces.value[0] ?? null;
|
|
12
|
+
return spaces.value.find((s) => s.doc_id === entryDocId) ?? spaces.value[0] ?? null;
|
|
14
13
|
});
|
|
15
14
|
async function refresh() {
|
|
16
15
|
loading.value = true;
|
|
@@ -5,9 +5,9 @@ export declare function useTableView(tree: {
|
|
|
5
5
|
treeMap: any;
|
|
6
6
|
}, tableDocId: string): {
|
|
7
7
|
tableMeta: import("vue").ComputedRef<DocPageMeta>;
|
|
8
|
-
explicitMode: import("vue").ComputedRef<"
|
|
9
|
-
detectedMode: import("vue").ComputedRef<"
|
|
10
|
-
effectiveMode: import("vue").ComputedRef<"
|
|
8
|
+
explicitMode: import("vue").ComputedRef<"hierarchy" | "flat" | undefined>;
|
|
9
|
+
detectedMode: import("vue").ComputedRef<"hierarchy" | "flat">;
|
|
10
|
+
effectiveMode: import("vue").ComputedRef<"hierarchy" | "flat">;
|
|
11
11
|
setMode: (mode: "hierarchy" | "flat" | undefined) => void;
|
|
12
12
|
columnDefs: import("vue").ComputedRef<TableColumnDef[]>;
|
|
13
13
|
addColumn: (def: TableColumnDef) => void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTypedDoc — typed reads + validated writes against the Abracadabra
|
|
3
|
+
* doc-tree, parameterised by a `@abraca/schema` registry.
|
|
4
|
+
*
|
|
5
|
+
* Day 2 of the Phase 3 schema rollout.
|
|
6
|
+
*
|
|
7
|
+
* The Nuxt module reads / writes the doc-tree through `useSyncedMap` on
|
|
8
|
+
* the root Y.Doc — it does NOT use `DocumentManager.docs(...)` from
|
|
9
|
+
* `@abraca/dabra`, because the module instantiates `AbracadabraProvider`
|
|
10
|
+
* directly (no DM wrapper). This composable is the Nuxt-side analogue:
|
|
11
|
+
* a typed projection over `useDocTree`'s reactive entries, with optional
|
|
12
|
+
* pre-write validation against the supplied registry.
|
|
13
|
+
*
|
|
14
|
+
* // app code
|
|
15
|
+
* import { kanbanSchema } from '@abraca/schema'
|
|
16
|
+
* const { get, all, update } = useTypedDoc(kanbanSchema)
|
|
17
|
+
* const board = computed(() => get('kanban', boardId.value))
|
|
18
|
+
* // board.value?.meta.kanbanColumnWidth is typed
|
|
19
|
+
* update('kanban', boardId.value, { kanbanColumnWidth: 'wide' })
|
|
20
|
+
*
|
|
21
|
+
* Behaviour:
|
|
22
|
+
* - `get(type, id)` returns null when the entry is missing OR its
|
|
23
|
+
* stored `type` doesn't match (cross-schema reads return null —
|
|
24
|
+
* matches the server-side smoke contract).
|
|
25
|
+
* - `update(type, id, patch)` validates the *merged* meta before
|
|
26
|
+
* writing. Throws `MetaValidationError`-equivalent on mismatch.
|
|
27
|
+
* - `set(type, id, meta)` validates the replacement meta.
|
|
28
|
+
* - `clear(type, id, keys)` is a thin wrapper over `updateMeta` —
|
|
29
|
+
* no validation (clearing keys can never fail validation).
|
|
30
|
+
* - Reads do NOT auto-call `runMigrations`. Pass `migrate: true` in
|
|
31
|
+
* the per-call options or use `useDocEntryTyped` (Day 3).
|
|
32
|
+
*
|
|
33
|
+
* Type inference: when a typed `SchemaRegistry<TMap>` is passed, every
|
|
34
|
+
* read/write narrows `meta` to `TMap[N]`. Untyped registries fall back
|
|
35
|
+
* to `Record<string, unknown>`.
|
|
36
|
+
*/
|
|
37
|
+
import { type ComputedRef } from 'vue';
|
|
38
|
+
import type { SchemaRegistryLike } from '../utils/schemaSupport.js';
|
|
39
|
+
/** Extract the doc-type names from a typed registry. */
|
|
40
|
+
type SchemaDocTypeName<S> = S extends {
|
|
41
|
+
__metaMap?: infer M;
|
|
42
|
+
} ? keyof M & string : string;
|
|
43
|
+
/** Extract the meta type for a given doc-type name from a registry. */
|
|
44
|
+
type SchemaMetaOf<S, N extends string> = S extends {
|
|
45
|
+
__metaMap?: infer M;
|
|
46
|
+
} ? N extends keyof M ? M[N] : Record<string, unknown> : Record<string, unknown>;
|
|
47
|
+
export interface TypedTreeEntry<M> {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly type: string;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly parentId: string | null;
|
|
52
|
+
readonly order: number;
|
|
53
|
+
readonly meta: M | undefined;
|
|
54
|
+
readonly createdAt: number | undefined;
|
|
55
|
+
readonly updatedAt: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown by typed write methods when validation fails.
|
|
59
|
+
*
|
|
60
|
+
* Mirrors `MetaValidationError` from `@abraca/dabra` — kept module-local
|
|
61
|
+
* because the Nuxt write path doesn't go through `MetaManager`.
|
|
62
|
+
* Consumers can `import { MetaValidationError } from '@abraca/nuxt'` to
|
|
63
|
+
* type-narrow without dragging in `@abraca/dabra`.
|
|
64
|
+
*/
|
|
65
|
+
export declare class MetaValidationError extends Error {
|
|
66
|
+
readonly docId: string;
|
|
67
|
+
readonly docType: string;
|
|
68
|
+
readonly errors: ReadonlyArray<{
|
|
69
|
+
path: ReadonlyArray<PropertyKey>;
|
|
70
|
+
message: string;
|
|
71
|
+
code?: string;
|
|
72
|
+
}>;
|
|
73
|
+
constructor(docId: string, docType: string, errors: ReadonlyArray<{
|
|
74
|
+
path: ReadonlyArray<PropertyKey>;
|
|
75
|
+
message: string;
|
|
76
|
+
code?: string;
|
|
77
|
+
}>);
|
|
78
|
+
}
|
|
79
|
+
export declare class TypedDocTypeMismatchError extends Error {
|
|
80
|
+
readonly docId: string;
|
|
81
|
+
readonly expectedType: string;
|
|
82
|
+
readonly actualType: string | undefined;
|
|
83
|
+
constructor(docId: string, expectedType: string, actualType: string | undefined);
|
|
84
|
+
}
|
|
85
|
+
export declare function useTypedDoc<S extends SchemaRegistryLike>(schema: S): {
|
|
86
|
+
/** Synchronous typed read. Returns null on missing entry or type mismatch. */
|
|
87
|
+
get: <N extends SchemaDocTypeName<S>>(type: N, id: string) => TypedTreeEntry<SchemaMetaOf<S, N>> | null;
|
|
88
|
+
/** Reactive list of every entry whose stored `type` matches `type`. */
|
|
89
|
+
all: <N extends SchemaDocTypeName<S>>(type: N) => ComputedRef<TypedTreeEntry<SchemaMetaOf<S, N>>[]>;
|
|
90
|
+
/** Validated meta merge. Throws on mismatch / validation failure. */
|
|
91
|
+
update: <N extends SchemaDocTypeName<S>>(type: N, id: string, patch: Partial<SchemaMetaOf<S, N>>) => void;
|
|
92
|
+
/** Validated meta replacement. Throws on mismatch / validation failure. */
|
|
93
|
+
set: <N extends SchemaDocTypeName<S>>(type: N, id: string, meta: SchemaMetaOf<S, N>) => void;
|
|
94
|
+
/** Delete specific keys. No validation (clearing can't fail validation). */
|
|
95
|
+
clear: <N extends SchemaDocTypeName<S>>(type: N, id: string, keys: ReadonlyArray<keyof SchemaMetaOf<S, N> & string>) => void;
|
|
96
|
+
};
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useAbracadabra } from "./useAbracadabra.js";
|
|
3
|
+
import { useDocTree } from "./useDocTree.js";
|
|
4
|
+
export class MetaValidationError extends Error {
|
|
5
|
+
docId;
|
|
6
|
+
docType;
|
|
7
|
+
errors;
|
|
8
|
+
constructor(docId, docType, errors) {
|
|
9
|
+
super(
|
|
10
|
+
`meta validation failed for doc ${docId} (type "${docType}"): ` + errors.map((e) => `${e.path.join(".") || "<root>"}: ${e.message}`).join("; ")
|
|
11
|
+
);
|
|
12
|
+
this.name = "MetaValidationError";
|
|
13
|
+
this.docId = docId;
|
|
14
|
+
this.docType = docType;
|
|
15
|
+
this.errors = errors;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class TypedDocTypeMismatchError extends Error {
|
|
19
|
+
docId;
|
|
20
|
+
expectedType;
|
|
21
|
+
actualType;
|
|
22
|
+
constructor(docId, expectedType, actualType) {
|
|
23
|
+
super(
|
|
24
|
+
`typed write on document ${docId} expected type "${expectedType}" but stored type is ` + (actualType === void 0 ? "(none)" : `"${actualType}"`)
|
|
25
|
+
);
|
|
26
|
+
this.name = "TypedDocTypeMismatchError";
|
|
27
|
+
this.docId = docId;
|
|
28
|
+
this.expectedType = expectedType;
|
|
29
|
+
this.actualType = actualType;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function projectEntry(entry, expectedType) {
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
if (entry.type !== expectedType) return null;
|
|
35
|
+
return {
|
|
36
|
+
id: entry.id,
|
|
37
|
+
type: expectedType,
|
|
38
|
+
label: entry.label,
|
|
39
|
+
parentId: entry.parentId,
|
|
40
|
+
order: entry.order,
|
|
41
|
+
meta: entry.meta,
|
|
42
|
+
createdAt: entry.createdAt,
|
|
43
|
+
updatedAt: entry.updatedAt
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function useTypedDoc(schema) {
|
|
47
|
+
const abra = useAbracadabra();
|
|
48
|
+
const tree = useDocTree();
|
|
49
|
+
function readEntry(id) {
|
|
50
|
+
return tree.getEntry(id);
|
|
51
|
+
}
|
|
52
|
+
function ensureType(id, expectedType) {
|
|
53
|
+
const entry = readEntry(id);
|
|
54
|
+
if (!entry) {
|
|
55
|
+
throw new TypedDocTypeMismatchError(id, expectedType, void 0);
|
|
56
|
+
}
|
|
57
|
+
if (entry.type !== expectedType) {
|
|
58
|
+
throw new TypedDocTypeMismatchError(id, expectedType, entry.type);
|
|
59
|
+
}
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
function validateOrThrow(id, type, meta) {
|
|
63
|
+
const result = schema.validateMeta(type, meta);
|
|
64
|
+
if (result.ok) return result.value ?? meta;
|
|
65
|
+
throw new MetaValidationError(id, type, result.errors);
|
|
66
|
+
}
|
|
67
|
+
function writeMeta(id, nextMeta) {
|
|
68
|
+
const doc = abra.doc.value;
|
|
69
|
+
if (!doc) return;
|
|
70
|
+
const treeMap = doc.getMap("doc-tree");
|
|
71
|
+
const entry = treeMap.get(id);
|
|
72
|
+
if (!entry) return;
|
|
73
|
+
treeMap.set(id, {
|
|
74
|
+
...entry,
|
|
75
|
+
meta: nextMeta,
|
|
76
|
+
updatedAt: Date.now()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
get(type, id) {
|
|
81
|
+
return projectEntry(readEntry(id), type);
|
|
82
|
+
},
|
|
83
|
+
all(type) {
|
|
84
|
+
return computed(
|
|
85
|
+
() => tree.entries.value.filter((e) => e.type === type && !e.trashed).map((e) => projectEntry(e, type)).filter((e) => e !== null)
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
update(type, id, patch) {
|
|
89
|
+
const entry = ensureType(id, type);
|
|
90
|
+
const merged = {
|
|
91
|
+
...entry.meta ?? {},
|
|
92
|
+
...patch
|
|
93
|
+
};
|
|
94
|
+
validateOrThrow(id, type, merged);
|
|
95
|
+
writeMeta(id, merged);
|
|
96
|
+
},
|
|
97
|
+
set(type, id, meta) {
|
|
98
|
+
ensureType(id, type);
|
|
99
|
+
validateOrThrow(id, type, meta);
|
|
100
|
+
writeMeta(id, meta);
|
|
101
|
+
},
|
|
102
|
+
clear(type, id, keys) {
|
|
103
|
+
const entry = ensureType(id, type);
|
|
104
|
+
if (!entry.meta) return;
|
|
105
|
+
const src = entry.meta;
|
|
106
|
+
const dropped = new Set(keys.map((k) => k));
|
|
107
|
+
const next = {};
|
|
108
|
+
for (const [k, v] of Object.entries(src)) {
|
|
109
|
+
if (!dropped.has(k)) next[k] = v;
|
|
110
|
+
}
|
|
111
|
+
writeMeta(id, next);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|