@3sln/trove 0.0.12 → 0.0.13
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/README.md +13 -1
- package/package.json +2 -1
- package/packages/core/src/collections/index.js +71 -10
- package/packages/core/src/encryption/keys.js +4 -3
- package/packages/core/src/encryption/policy.js +14 -7
- package/packages/core/src/encryption/rotation.js +46 -12
- package/packages/core/src/errors.js +8 -0
- package/packages/core/src/index.js +8 -2
- package/packages/core/src/indexers/contribution.js +18 -17
- package/packages/core/src/indexing.js +8 -4
- package/packages/core/src/issues.js +8 -0
- package/packages/core/src/links.js +21 -9
- package/packages/core/src/metadata/interface.js +22 -0
- package/packages/core/src/metadata/memory.js +15 -0
- package/packages/core/src/metadata/sqlite.js +23 -1
- package/packages/core/src/plugins/contributions.js +13 -0
- package/packages/core/src/rateLimit.js +253 -0
- package/packages/core/src/scan.js +44 -16
- package/packages/core/src/sidecar/document.js +18 -14
- package/packages/core/src/sidecar/index.js +9 -10
- package/packages/core/src/sidecar/manager.js +10 -15
- package/packages/core/src/uploads.js +40 -15
- package/packages/core/src/vfs.js +59 -34
- package/packages/plugin-sdk/src/browser.js +155 -0
- package/packages/plugin-sdk/src/protocol.js +13 -1
- package/packages/server/src/adapters/bun.js +1 -2
- package/packages/server/src/adapters/node.js +1 -2
- package/packages/server/src/engine/index.js +1 -1
- package/packages/server/src/engine/providers/access.js +17 -34
- package/packages/server/src/engine/providers/core.js +112 -14
- package/packages/server/src/index.js +145 -103
- package/packages/server/src/mcp/index.js +7 -12
- package/packages/server/src/mcp/tools.js +24 -29
- package/packages/server/src/router.js +31 -8
- package/packages/server/src/routes.js +74 -113
- package/packages/server/src/scope.js +78 -0
- package/packages/web/dist/assets/main-828yzsr7.js +511 -0
- package/packages/web/dist/assets/main-828yzsr7.js.map +120 -0
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/src/bl/actions.js +226 -104
- package/packages/web/src/bl/activity.js +32 -82
- package/packages/web/src/bl/index.js +23 -11
- package/packages/web/src/bl/launcher.js +2 -9
- package/packages/web/src/bl/links.js +31 -18
- package/packages/web/src/bl/offline.js +24 -10
- package/packages/web/src/bl/pluginInstall.js +29 -26
- package/packages/web/src/bl/queries.js +76 -68
- package/packages/web/src/bl/services.js +22 -14
- package/packages/web/src/bl/social.js +39 -25
- package/packages/web/src/bl/state.js +22 -4
- package/packages/web/src/bl/viewState.js +9 -0
- package/packages/web/src/bl/watchQuery.js +1 -8
- package/packages/web/src/dispatch.js +26 -0
- package/packages/web/src/platform/api.js +32 -0
- package/packages/web/src/platform/commands.js +14 -15
- package/packages/web/src/platform/context.js +3 -58
- package/packages/web/src/platform/contributions.js +1 -35
- package/packages/web/src/platform/fileChunks.js +281 -0
- package/packages/web/src/platform/index.js +8 -9
- package/packages/web/src/platform/mediaUrls.js +10 -0
- package/packages/web/src/platform/navigation.js +35 -21
- package/packages/web/src/platform/pickers.js +39 -19
- package/packages/web/src/platform/pluginHost.js +4 -1
- package/packages/web/src/platform/pluginRpc.js +47 -2
- package/packages/web/src/platform/spatialNav.js +1 -1
- package/packages/web/src/platform/viewport.js +14 -6
- package/packages/web/src/platform/voiceSearch.js +17 -9
- package/packages/web/src/platform/whenclause.js +9 -19
- package/packages/web/src/runtime.js +2 -37
- package/packages/web/src/ui/activate.js +18 -2
- package/packages/web/src/ui/components/activityPanel.js +1 -1
- package/packages/web/src/ui/components/adminView.js +4 -7
- package/packages/web/src/ui/components/editorArea.js +10 -1
- package/packages/web/src/ui/components/launcher.js +29 -13
- package/packages/web/src/ui/components/openers/index.js +17 -22
- package/packages/web/src/ui/components/openers/markdown.js +2 -2
- package/packages/web/src/ui/components/overlays.js +31 -5
- package/packages/web/src/ui/components/pluginReview.js +7 -4
- package/packages/web/src/ui/components/pluginsView.js +1 -1
- package/packages/web/src/ui/components/settingsView.js +30 -27
- package/packages/web/src/ui/components/statusBar.js +8 -6
- package/packages/web/src/ui/components/views/grid.js +2 -2
- package/packages/web/src/ui/components/views/parts.js +1 -1
- package/packages/web/src/ui/compositions/workbench.js +20 -6
- package/packages/web/src/ui/sanitize.js +0 -5
- package/packages/web/src/workbench.js +37 -19
- package/packages/web/dist/assets/main-c9dnnnc6.js +0 -356
- package/packages/web/dist/assets/main-c9dnnnc6.js.map +0 -118
package/packages/web/dist/sw.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// API and FILES deliberately do NOT rotate. API is data, not code, and FILES holds the
|
|
19
19
|
// bytes of files the user pinned for offline use — naming it per build would throw away
|
|
20
20
|
// someone's offline library every time the app was redeployed.
|
|
21
|
-
const SHELL = 'trove-shell-
|
|
21
|
+
const SHELL = 'trove-shell-fbe8c37315';
|
|
22
22
|
const API = 'trove-api-v1';
|
|
23
23
|
const FILES = 'trove-files-v1';
|
|
24
24
|
const KEEP = new Set([SHELL, API, FILES]);
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// they update flow straight back into the UI via `watch`.
|
|
6
6
|
|
|
7
7
|
import { Action } from '@3sln/ngin';
|
|
8
|
-
import {
|
|
8
|
+
import { runAction } from '../dispatch.js';
|
|
9
|
+
import { PROMPT, PLUGIN_REVIEW } from './viewState.js';
|
|
9
10
|
import { beginInstallFromUrl } from './pluginInstall.js';
|
|
10
11
|
import { newId } from '@3sln/trove/core/util.js';
|
|
11
12
|
import { matchesTagFilters } from './tagQuery.js';
|
|
@@ -59,7 +60,7 @@ export class NavigateAction extends Action {
|
|
|
59
60
|
nextCursor: res.nextCursor || null,
|
|
60
61
|
});
|
|
61
62
|
// Remember where they were, so the next visit opens there rather than guessing.
|
|
62
|
-
settings.set
|
|
63
|
+
settings.set('explorer.lastCollection', collectionId);
|
|
63
64
|
explorer.set({ gate: null });
|
|
64
65
|
// Explorer→context projection (collectionId/hasSelection) lives in bl/index.js
|
|
65
66
|
// so it stays in sync with selection too — nothing to mirror here.
|
|
@@ -186,7 +187,9 @@ export class OpenInitialCollectionAction extends Action {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
explorer.set({ gate: null });
|
|
189
|
-
|
|
190
|
+
// Sequenced, so the panel this opens below lands on top of the collection rather than
|
|
191
|
+
// racing its load — see src/dispatch.js for why a bare `await` here did nothing.
|
|
192
|
+
await runAction(engine, new NavigateAction(shared.collection));
|
|
190
193
|
|
|
191
194
|
let node;
|
|
192
195
|
try {
|
|
@@ -275,7 +278,10 @@ export class CreateCollectionAction extends Action {
|
|
|
275
278
|
try {
|
|
276
279
|
const res = await r.api.createCollection(this.record);
|
|
277
280
|
r.notifications.success(`Created collection “${res.collection.name}”`);
|
|
278
|
-
|
|
281
|
+
// Sequenced: `NavigateAction` writes `collectionId` synchronously while the load
|
|
282
|
+
// writes `collections` after a round trip, so navigating first left the status bar's
|
|
283
|
+
// `collectionLabelOf` with nothing to look the id up in — it showed a raw `col_…`.
|
|
284
|
+
await runAction(r.engine, new LoadCollectionsAction());
|
|
279
285
|
r.engine.dispatch(new NavigateAction(res.collection.id));
|
|
280
286
|
} catch (err) {
|
|
281
287
|
r.notifications.error(`Couldn’t create collection: ${err.message}`);
|
|
@@ -348,7 +354,7 @@ export class TrashAction extends Action {
|
|
|
348
354
|
const { items } = await api.trash(collection);
|
|
349
355
|
explorer.set({ trash: items });
|
|
350
356
|
// Restoring puts something back in the drive, so the list on screen is now stale.
|
|
351
|
-
if (this.op !== 'list')
|
|
357
|
+
if (this.op !== 'list') r.engine.dispatch(new NavigateAction(collection));
|
|
352
358
|
} catch (err) {
|
|
353
359
|
notifications.error(`Trash: ${err.message}`);
|
|
354
360
|
}
|
|
@@ -518,7 +524,7 @@ export class FilterAction extends Action {
|
|
|
518
524
|
return;
|
|
519
525
|
}
|
|
520
526
|
search.set({ query: this.text, loading: true, error: null, filtered: true });
|
|
521
|
-
if (r.offline && !r.offline.
|
|
527
|
+
if (r.offline && !r.offline.get().online) {
|
|
522
528
|
const items = (r.explorer.get().items || []).filter((n) => matchesTagFilters(n, this.filters));
|
|
523
529
|
search.set({ results: items.map((node) => ({ node })), loading: false, ran: true, filtered: true, offline: true });
|
|
524
530
|
return;
|
|
@@ -583,14 +589,23 @@ export class SearchAction extends Action {
|
|
|
583
589
|
return;
|
|
584
590
|
}
|
|
585
591
|
const mode = this.mode || settings.get('search.mode');
|
|
586
|
-
search.set({ query: this.query, mode, loading: true, error: null, resolved: null });
|
|
592
|
+
search.set({ query: this.query, mode, loading: true, error: null, resolved: null, filtered: false });
|
|
593
|
+
// The query itself is the sequence token, the same guard QuickOpenAction uses and for
|
|
594
|
+
// the same reason: keystrokes outrun the network, and a slower request for an earlier
|
|
595
|
+
// query must not land on top of a newer one's results. It matters most for the query
|
|
596
|
+
// that was CLEARED — pressing Escape inside the debounce window used to let
|
|
597
|
+
// `SearchAction('contract')` land after the clear, leaving "Searching for contract"
|
|
598
|
+
// over the home list.
|
|
599
|
+
const superseded = () => search.get().query !== this.query;
|
|
587
600
|
const offline = r.offline;
|
|
588
601
|
// Offline (or server unreachable) → search the pinned corpus locally.
|
|
589
|
-
if (offline && !offline.
|
|
602
|
+
if (offline && !offline.get().online) {
|
|
590
603
|
try {
|
|
591
604
|
const results = await offline.searchOffline(q, { limit: 40 });
|
|
605
|
+
if (superseded()) return;
|
|
592
606
|
search.set({ results, loading: false, ran: true, offline: true, resolved: null });
|
|
593
607
|
} catch (err) {
|
|
608
|
+
if (superseded()) return;
|
|
594
609
|
search.set({ results: [], loading: false, ran: true, offline: true, error: err.message });
|
|
595
610
|
}
|
|
596
611
|
return;
|
|
@@ -605,10 +620,13 @@ export class SearchAction extends Action {
|
|
|
605
620
|
// content types. It can only name a view from this list.
|
|
606
621
|
const views = contributions.ofType('view').map((v) => ({ id: v.id, title: v.title || v.id }));
|
|
607
622
|
const res = await api.query(q, { mode, limit: 40, views });
|
|
623
|
+
if (superseded()) return;
|
|
608
624
|
search.set({ results: res.results, resolved: res.resolved || null, loading: false, ran: true, offline: false });
|
|
609
625
|
} catch (err) {
|
|
626
|
+
if (superseded()) return;
|
|
610
627
|
if (offline) {
|
|
611
628
|
const results = await offline.searchOffline(q, { limit: 40 });
|
|
629
|
+
if (superseded()) return;
|
|
612
630
|
search.set({ results, loading: false, ran: true, offline: true, error: results.length ? null : err.message });
|
|
613
631
|
} else {
|
|
614
632
|
search.set({ loading: false, error: err.message, ran: true });
|
|
@@ -622,9 +640,14 @@ export class SearchAction extends Action {
|
|
|
622
640
|
/**
|
|
623
641
|
* Load the key list.
|
|
624
642
|
*
|
|
625
|
-
* Silent on
|
|
626
|
-
* opening Settings gets a 403 that is the correct answer, not an error worth a toast.
|
|
627
|
-
*
|
|
643
|
+
* Silent on REFUSAL rather than noisy: this fires when Settings opens, and a non-admin
|
|
644
|
+
* opening Settings gets a 403 that is the correct answer, not an error worth a toast. That
|
|
645
|
+
* is `loaded` with an empty list — the section simply is not part of their settings screen.
|
|
646
|
+
*
|
|
647
|
+
* Any other failure sets `error` and leaves `loaded` alone, because they are different
|
|
648
|
+
* facts. Setting both meant one transient failure rendered the section blank with no
|
|
649
|
+
* message and no way back: `loaded` was true so the "not yet" branch passed, and the empty
|
|
650
|
+
* list was indistinguishable from a drive with no keys.
|
|
628
651
|
*/
|
|
629
652
|
export class LoadApiKeysAction extends Action {
|
|
630
653
|
static deps = ['api', 'apiKeys'];
|
|
@@ -635,7 +658,11 @@ export class LoadApiKeysAction extends Action {
|
|
|
635
658
|
const res = await r.api.apiKeys();
|
|
636
659
|
r.apiKeys.set({ keys: res.keys || [], loading: false, loaded: true, error: null });
|
|
637
660
|
} catch (err) {
|
|
638
|
-
|
|
661
|
+
if (err?.status === 403 || err?.code === 'forbidden') {
|
|
662
|
+
r.apiKeys.set({ keys: [], loading: false, loaded: true, error: null, forbidden: true });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
r.apiKeys.set({ loading: false, error: err?.message || 'Could not load API keys' });
|
|
639
666
|
}
|
|
640
667
|
}
|
|
641
668
|
}
|
|
@@ -752,21 +779,6 @@ export class CopyTextAction extends Action {
|
|
|
752
779
|
}
|
|
753
780
|
}
|
|
754
781
|
|
|
755
|
-
/** Say something to the user. `kind` is one of info, success, warn, error. */
|
|
756
|
-
export class NotifyAction extends Action {
|
|
757
|
-
static deps = ['notifications'];
|
|
758
|
-
constructor(kind, message, opts) {
|
|
759
|
-
super();
|
|
760
|
-
this.kind = kind;
|
|
761
|
-
this.message = message;
|
|
762
|
-
this.opts = opts;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
async execute({ notifications: notes }) {
|
|
766
|
-
(notes[this.kind] ?? notes.info).call(notes, this.message, this.opts);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
782
|
/** Dismiss one notification, by id. */
|
|
771
783
|
export class DismissNotificationAction extends Action {
|
|
772
784
|
static deps = ['notifications'];
|
|
@@ -791,11 +803,11 @@ export class DismissNotificationAction extends Action {
|
|
|
791
803
|
// discriminated blob would make every one of them read as "ShellAction" and give back
|
|
792
804
|
// exactly the observability this is for.
|
|
793
805
|
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
806
|
+
// `showDialog` and `showContextMenu` are here too, and the reason they arrived last is the
|
|
807
|
+
// reason they are worth having: both used to take callbacks — a dialog carried `onConfirm`,
|
|
808
|
+
// a menu carried an item's `run` — so converting them mechanically would have posted a
|
|
809
|
+
// closure through the engine and called it an action. Every outcome is an ACTION TO
|
|
810
|
+
// DISPATCH now, which is why nothing in a dialog spec or a menu item is callable.
|
|
799
811
|
|
|
800
812
|
class ShellAction extends Action {
|
|
801
813
|
static deps = ['workbench'];
|
|
@@ -967,9 +979,22 @@ class ActivityAction extends Action {
|
|
|
967
979
|
static deps = ['activity'];
|
|
968
980
|
async execute(r) { this.apply(r.activity); }
|
|
969
981
|
}
|
|
970
|
-
|
|
982
|
+
/**
|
|
983
|
+
* Show or hide the activity panel.
|
|
984
|
+
*
|
|
985
|
+
* The FLAG is overlay state like every other transient surface; the REFRESH is the
|
|
986
|
+
* poller's. Opening has to refresh because polling backs off to 60s when nothing is
|
|
987
|
+
* running, so a panel opened on an idle drive would otherwise show a list up to a minute
|
|
988
|
+
* stale — which is exactly the moment someone is looking to find out what just happened.
|
|
989
|
+
*/
|
|
990
|
+
export class ToggleActivityPanelAction extends Action {
|
|
991
|
+
static deps = ['activity', 'overlay'];
|
|
971
992
|
constructor(which) { super(); this.which = which; }
|
|
972
|
-
|
|
993
|
+
async execute({ activity, overlay }) {
|
|
994
|
+
const open = this.which ?? !overlay.get().activityPanel;
|
|
995
|
+
overlay.set({ activityPanel: open });
|
|
996
|
+
if (open) await activity.refresh();
|
|
997
|
+
}
|
|
973
998
|
}
|
|
974
999
|
export class CancelTaskAction extends ActivityAction {
|
|
975
1000
|
constructor(id) { super(); this.id = id; }
|
|
@@ -1100,37 +1125,6 @@ export class ToggleApiKeyCapAction extends ApiKeysAction {
|
|
|
1100
1125
|
}
|
|
1101
1126
|
}
|
|
1102
1127
|
|
|
1103
|
-
/**
|
|
1104
|
-
* Select items in the explorer.
|
|
1105
|
-
*
|
|
1106
|
-
* `opts` is `{ additive, nodes }`, matching the service — NOT a boolean. It was written as
|
|
1107
|
-
* `additive` first, which worked at the one call site only because the options object passed
|
|
1108
|
-
* through positionally unchanged. `new SelectItemsAction(ids, true)` would have destructured
|
|
1109
|
-
* a boolean into `{additive = false, nodes = null}` and quietly not been additive at all.
|
|
1110
|
-
*
|
|
1111
|
-
* `nodes` matters when the selection cannot be resolved from the loaded page: the launcher's
|
|
1112
|
-
* rows come from search, which is scoped across collections, so the node has to travel with
|
|
1113
|
-
* the id.
|
|
1114
|
-
*/
|
|
1115
|
-
export class SelectItemsAction extends Action {
|
|
1116
|
-
static deps = ['explorer'];
|
|
1117
|
-
|
|
1118
|
-
constructor(ids, opts = {}) { super(); this.ids = ids; this.opts = opts; }
|
|
1119
|
-
|
|
1120
|
-
async execute({ explorer }) {
|
|
1121
|
-
const { additive = false, nodes = null } = this.opts;
|
|
1122
|
-
const current = explorer.get().selection;
|
|
1123
|
-
const next = additive ? Array.from(new Set([...current, ...this.ids])) : this.ids;
|
|
1124
|
-
// Selecting what is already selected must not emit. The launcher syncs the highlighted
|
|
1125
|
-
// row into here on every mouseenter, and a state push per mouse move would re-render
|
|
1126
|
-
// the list under the pointer. The rule lives with the mutation now rather than inside
|
|
1127
|
-
// the resource, so the resource only holds and only `set` writes.
|
|
1128
|
-
const same = next.length === current.length && next.every((id, i) => id === current[i]);
|
|
1129
|
-
if (same) return;
|
|
1130
|
-
explorer.set({ selection: next, selectionNodes: nodes && !additive ? nodes : null });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
1128
|
/**
|
|
1135
1129
|
* Move the launcher's highlight and sync the selection to whatever it landed on.
|
|
1136
1130
|
*
|
|
@@ -1166,7 +1160,19 @@ export class SelectLaunchAction extends Action {
|
|
|
1166
1160
|
}
|
|
1167
1161
|
}
|
|
1168
1162
|
|
|
1169
|
-
/**
|
|
1163
|
+
/**
|
|
1164
|
+
* Select exactly one node, or nothing.
|
|
1165
|
+
*
|
|
1166
|
+
* `selectionNodes` carries the NODE and not just the id, because the selection often cannot
|
|
1167
|
+
* be resolved from the loaded page: the launcher's rows come from search, which is scoped
|
|
1168
|
+
* across collections, so the node has to travel with the id or `selectedNodesOf` finds
|
|
1169
|
+
* nothing and rename/trash/copy-link return quietly while `hasSelection` disagrees.
|
|
1170
|
+
*
|
|
1171
|
+
* Selecting what is already selected must not emit. The launcher syncs the highlighted row
|
|
1172
|
+
* into here on every mouseenter, and a state push per mouse move would re-render the list
|
|
1173
|
+
* under the pointer. The rule lives with the mutation rather than inside the resource, so
|
|
1174
|
+
* the resource only holds and only `set` writes.
|
|
1175
|
+
*/
|
|
1170
1176
|
function selectNode(explorer, node) {
|
|
1171
1177
|
const ids = node?.id ? [node.id] : [];
|
|
1172
1178
|
const current = explorer.get().selection;
|
|
@@ -1196,8 +1202,26 @@ export class SetViewStateAction extends Action {
|
|
|
1196
1202
|
* the feed. See ui/activate.js.
|
|
1197
1203
|
*/
|
|
1198
1204
|
export class ShowContextMenuAction extends OverlayAction {
|
|
1199
|
-
|
|
1200
|
-
|
|
1205
|
+
/**
|
|
1206
|
+
* @param {Array} items
|
|
1207
|
+
* @param {{x?: number, y?: number, rect?: DOMRect, prefer?: 'up'|'down'}|null} [at]
|
|
1208
|
+
* A POINT (a right-click) or a rect to hang off (a button), not a resolved position.
|
|
1209
|
+
* Callers used to resolve it themselves — `r.top - 8 - items.length * 34`, a row height
|
|
1210
|
+
* hardcoded in two files that had to agree with the CSS and did not have to agree with
|
|
1211
|
+
* the overlay's clamp, which compensated by a different amount. Only the overlay can
|
|
1212
|
+
* measure, so only the overlay decides. Omit `at` for a keyboard-invoked menu.
|
|
1213
|
+
*/
|
|
1214
|
+
constructor(items, at = null) {
|
|
1215
|
+
super();
|
|
1216
|
+
this.items = items;
|
|
1217
|
+
// Flattened to plain numbers: a DOMRect is a live object with methods, and this lands
|
|
1218
|
+
// in engine state, where nothing is callable.
|
|
1219
|
+
this.at = at?.rect
|
|
1220
|
+
? { prefer: at.prefer || 'down', rect: { left: at.rect.left, top: at.rect.top, right: at.rect.right, bottom: at.rect.bottom } }
|
|
1221
|
+
: (at ? { x: at.x, y: at.y } : null);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
apply(o) { o.set({ contextMenu: { items: this.items, at: this.at } }); }
|
|
1201
1225
|
}
|
|
1202
1226
|
|
|
1203
1227
|
/**
|
|
@@ -1279,22 +1303,30 @@ export class SetPluginSecretAction extends Action {
|
|
|
1279
1303
|
* lost access to. Either way the click must say so rather than do nothing.
|
|
1280
1304
|
*/
|
|
1281
1305
|
export class OpenNotificationTargetAction extends Action {
|
|
1282
|
-
|
|
1306
|
+
// Five of these used to exist only to feed a hand-call of `OpenFileAction.execute(r)` —
|
|
1307
|
+
// the one `.execute(` in the package — and one of them, `explorer`, fed nothing at all.
|
|
1308
|
+
// The coupling was invisible: a dep added to OpenFileAction broke THIS action at runtime
|
|
1309
|
+
// with an `undefined`, the same failure mode as the missing `overlay` above. Dispatching
|
|
1310
|
+
// instead also puts the open on the feed, which it never was: opening from a notification
|
|
1311
|
+
// and opening from a row click are the same intent and the engine should see both.
|
|
1312
|
+
static deps = ['api', 'engine', 'notifications', 'workbench'];
|
|
1283
1313
|
|
|
1284
1314
|
constructor(nodeId) { super(); this.nodeId = nodeId; }
|
|
1285
1315
|
|
|
1286
|
-
async execute(
|
|
1316
|
+
async execute({ api, engine, notifications, workbench }) {
|
|
1287
1317
|
let node;
|
|
1288
1318
|
try {
|
|
1289
|
-
({ node } = await
|
|
1319
|
+
({ node } = await api.stat(this.nodeId));
|
|
1290
1320
|
} catch (err) {
|
|
1291
|
-
|
|
1321
|
+
notifications.warn(err?.status === 403 || err?.code === 'forbidden'
|
|
1292
1322
|
? 'You no longer have access to that item.'
|
|
1293
1323
|
: 'That item no longer exists.');
|
|
1294
1324
|
return;
|
|
1295
1325
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1326
|
+
// `.next([...])`, because `dispatch` returns a feed and the panel must open AFTER the
|
|
1327
|
+
// file does — see platform/commands.js, which names this trap.
|
|
1328
|
+
await runAction(engine, new OpenFileAction(node));
|
|
1329
|
+
workbench.set({ infoPanel: true });
|
|
1298
1330
|
}
|
|
1299
1331
|
}
|
|
1300
1332
|
|
|
@@ -1321,7 +1353,42 @@ export class RenamePromptAction extends PromptAction {
|
|
|
1321
1353
|
static deps = ['engine', 'overlay', 'viewState'];
|
|
1322
1354
|
constructor(node) { super(); this.node = node; }
|
|
1323
1355
|
async withValue(name, { engine }) {
|
|
1324
|
-
if (name !== this.node.name)
|
|
1356
|
+
if (name !== this.node.name) engine.dispatch(new RenameAction(this.node.id, name));
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Install the package the review dialog is about, with the capabilities that were ticked.
|
|
1362
|
+
*
|
|
1363
|
+
* The dialog used to carry an `onInstall` closure that did all of this, which put an effect
|
|
1364
|
+
* inside engine state — and ran it after the dispatching action's lease had been released,
|
|
1365
|
+
* with nothing of it on the feed. Two docblocks assert that a dialog spec holds no
|
|
1366
|
+
* functions; this is what makes that true.
|
|
1367
|
+
*
|
|
1368
|
+
* `pkg` and `trust` ride on the instance because they are what the review WAS; the grants
|
|
1369
|
+
* are read from viewState, because they are what the user did to it while it was open.
|
|
1370
|
+
*/
|
|
1371
|
+
export class InstallReviewedPluginAction extends Action {
|
|
1372
|
+
static deps = ['notifications', 'overlay', 'plugins', 'viewState', 'workbench'];
|
|
1373
|
+
|
|
1374
|
+
constructor(pkg, trust, label) { super(); this.pkg = pkg; this.trust = trust; this.label = label; }
|
|
1375
|
+
|
|
1376
|
+
async execute({ notifications, overlay, plugins, viewState, workbench }) {
|
|
1377
|
+
const grants = [...(viewState.get()[PLUGIN_REVIEW]?.grants || [])];
|
|
1378
|
+
overlay.set({ dialog: null });
|
|
1379
|
+
// Account installs upload the package and run a handshake that can take a while —
|
|
1380
|
+
// switch to the plugins view (which shows the plugin's loading state) and hold a sticky
|
|
1381
|
+
// "Installing…" toast so the user isn't left staring at nothing.
|
|
1382
|
+
workbench.set({ activity: 'plugins', sidebarVisible: true });
|
|
1383
|
+
const pending = notifications.info(`Installing “${this.label}”…`, { sticky: true });
|
|
1384
|
+
try {
|
|
1385
|
+
await plugins.install(this.pkg, { grants, trust: this.trust });
|
|
1386
|
+
notifications.dismiss(pending);
|
|
1387
|
+
notifications.success(`Installed “${this.label}”`);
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
notifications.dismiss(pending);
|
|
1390
|
+
notifications.error(`Install failed: ${err.message}`);
|
|
1391
|
+
}
|
|
1325
1392
|
}
|
|
1326
1393
|
}
|
|
1327
1394
|
|
|
@@ -1344,7 +1411,7 @@ export class CreateCollectionFromFormAction extends Action {
|
|
|
1344
1411
|
constructor(record) { super(); this.record = record; }
|
|
1345
1412
|
async execute({ engine, overlay }) {
|
|
1346
1413
|
overlay.set({ dialog: null });
|
|
1347
|
-
if (this.record)
|
|
1414
|
+
if (this.record) engine.dispatch(new CreateCollectionAction(this.record));
|
|
1348
1415
|
}
|
|
1349
1416
|
}
|
|
1350
1417
|
|
|
@@ -1431,8 +1498,8 @@ export class SetActivityAction extends ShellAction {
|
|
|
1431
1498
|
* once, here, rather than distributed over whoever owns each overlay.
|
|
1432
1499
|
*/
|
|
1433
1500
|
export class CloseOverlaysAction extends Action {
|
|
1434
|
-
static deps = ['
|
|
1435
|
-
async execute({
|
|
1501
|
+
static deps = ['navigation', 'overlay', 'workbench'];
|
|
1502
|
+
async execute({ navigation, overlay, workbench }) {
|
|
1436
1503
|
const o = overlay.get();
|
|
1437
1504
|
const wb = workbench.get();
|
|
1438
1505
|
if (o.contextMenu) return overlay.set({ contextMenu: null });
|
|
@@ -1446,9 +1513,9 @@ export class CloseOverlaysAction extends Action {
|
|
|
1446
1513
|
// plugin panel deliberately: it opens BY ITSELF when a storage check finishes, and
|
|
1447
1514
|
// Escape should not close what you opened on purpose in order to dismiss what
|
|
1448
1515
|
// appeared on its own.
|
|
1449
|
-
if (
|
|
1516
|
+
if (o.activityPanel) return overlay.set({ activityPanel: false });
|
|
1450
1517
|
// Nothing floating: Escape pops the top viewer panel instead.
|
|
1451
|
-
if (navigation.
|
|
1518
|
+
if (navigation.get().stack.length > 1) navigation.back();
|
|
1452
1519
|
}
|
|
1453
1520
|
}
|
|
1454
1521
|
|
|
@@ -1479,8 +1546,20 @@ export class VoiceSearchAction extends Action {
|
|
|
1479
1546
|
|
|
1480
1547
|
/** Reindex the whole drive. Reports as a task rather than blocking — it takes minutes. */
|
|
1481
1548
|
export class RebuildIndexAction extends Action {
|
|
1482
|
-
static deps = ['activity'];
|
|
1483
|
-
async execute({ activity
|
|
1549
|
+
static deps = ['activity', 'api', 'engine', 'notifications'];
|
|
1550
|
+
async execute({ activity, api, engine, notifications }) {
|
|
1551
|
+
try {
|
|
1552
|
+
const res = await api.reindex();
|
|
1553
|
+
await activity.refreshTasks();
|
|
1554
|
+
// "…and show the user" belongs to the action that decided to do the work, not to
|
|
1555
|
+
// the service that holds the list. Three network verbs used to open the panel as a
|
|
1556
|
+
// side effect of a fetch, from inside a resource.
|
|
1557
|
+
engine.dispatch(new ToggleActivityPanelAction(true));
|
|
1558
|
+
if (res.alreadyRunning) notifications.info('A rebuild is already running');
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
notifications.error(`Couldn't rebuild the index: ${err.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1484
1563
|
}
|
|
1485
1564
|
|
|
1486
1565
|
/**
|
|
@@ -1491,11 +1570,19 @@ export class RebuildIndexAction extends Action {
|
|
|
1491
1570
|
* a scan error.
|
|
1492
1571
|
*/
|
|
1493
1572
|
export class ScanCollectionAction extends Action {
|
|
1494
|
-
static deps = ['activity', 'explorer', 'notifications'];
|
|
1495
|
-
async execute({ activity, explorer, notifications }) {
|
|
1573
|
+
static deps = ['activity', 'api', 'engine', 'explorer', 'notifications'];
|
|
1574
|
+
async execute({ activity, api, engine, explorer, notifications }) {
|
|
1496
1575
|
const id = explorer.get().collectionId;
|
|
1497
1576
|
if (!id) return notifications.info('Open a collection first — a scan is per collection.');
|
|
1498
|
-
|
|
1577
|
+
try {
|
|
1578
|
+
const res = await api.scanCollection(id);
|
|
1579
|
+
await activity.refreshTasks();
|
|
1580
|
+
engine.dispatch(new ToggleActivityPanelAction(true));
|
|
1581
|
+
if (res.alreadyRunning) notifications.info('A scan of this collection is already running');
|
|
1582
|
+
activity.followUp();
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
notifications.error(`Couldn't scan “${id}”: ${err.message}`);
|
|
1585
|
+
}
|
|
1499
1586
|
}
|
|
1500
1587
|
}
|
|
1501
1588
|
|
|
@@ -1506,8 +1593,28 @@ export class ScanCollectionAction extends Action {
|
|
|
1506
1593
|
* from here — the failure that makes every file open to a spinner.
|
|
1507
1594
|
*/
|
|
1508
1595
|
export class CheckStorageAction extends Action {
|
|
1509
|
-
static deps = ['activity'];
|
|
1510
|
-
async execute({ activity
|
|
1596
|
+
static deps = ['activity', 'api', 'engine', 'notifications'];
|
|
1597
|
+
async execute({ activity, api, engine, notifications }) {
|
|
1598
|
+
try {
|
|
1599
|
+
const res = await api.checkStorage();
|
|
1600
|
+
await activity.refresh();
|
|
1601
|
+
engine.dispatch(new ToggleActivityPanelAction(true));
|
|
1602
|
+
const problems = (res.results || []).reduce((n, r) => n + (r.findings?.length || 0), 0);
|
|
1603
|
+
if (problems) {
|
|
1604
|
+
notifications.warn(`Found ${problems} storage problem${problems === 1 ? '' : 's'} — see Activity`);
|
|
1605
|
+
} else if (!res.checked) {
|
|
1606
|
+
notifications.info('There are no collections to check yet');
|
|
1607
|
+
} else if (!res.corsChecked) {
|
|
1608
|
+
// Honest about what was not checked. Reporting "all good" having skipped the check
|
|
1609
|
+
// that matters is how a diagnostic stops being believed.
|
|
1610
|
+
notifications.info('Stores are reachable. Browser access was not checked — this drive has no public URL configured.');
|
|
1611
|
+
} else {
|
|
1612
|
+
notifications.success('Stores are reachable and allow browser access');
|
|
1613
|
+
}
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
notifications.error(`Couldn't check the stores: ${err.message}`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1511
1618
|
}
|
|
1512
1619
|
|
|
1513
1620
|
/** Ask for files, then upload them into the open collection. */
|
|
@@ -1515,7 +1622,10 @@ export class PickAndUploadAction extends Action {
|
|
|
1515
1622
|
static deps = ['engine', 'explorer'];
|
|
1516
1623
|
async execute({ engine, explorer }) {
|
|
1517
1624
|
const collection = explorer.get().collectionId;
|
|
1518
|
-
|
|
1625
|
+
// Awaited, so the feed's `complete` means the upload started rather than the dialog
|
|
1626
|
+
// opened — and so the leases below it are still held when the upload uses them.
|
|
1627
|
+
const files = await pickFiles();
|
|
1628
|
+
if (files?.length) await runAction(engine, new UploadFilesAction(files, collection));
|
|
1519
1629
|
}
|
|
1520
1630
|
}
|
|
1521
1631
|
|
|
@@ -1637,18 +1747,14 @@ export class PinAction extends Action {
|
|
|
1637
1747
|
|
|
1638
1748
|
// --- API keys (admin) ------------------------------------------------------------
|
|
1639
1749
|
|
|
1640
|
-
class
|
|
1641
|
-
static deps = ['apiKeys'];
|
|
1642
|
-
async execute({ apiKeys }) { this.apply(apiKeys); }
|
|
1643
|
-
}
|
|
1644
|
-
export class StartApiKeyDraftAction extends ApiKeyDraftAction {
|
|
1750
|
+
export class StartApiKeyDraftAction extends ApiKeysAction {
|
|
1645
1751
|
apply(s) { s.set({ draft: { name: '', expiresInDays: '', caps: {} }, error: null }); }
|
|
1646
1752
|
}
|
|
1647
|
-
export class CancelApiKeyDraftAction extends
|
|
1753
|
+
export class CancelApiKeyDraftAction extends ApiKeysAction {
|
|
1648
1754
|
apply(s) { s.set({ draft: null }); }
|
|
1649
1755
|
}
|
|
1650
1756
|
/** Forget the freshly minted secret. On dismiss, and after a copy. */
|
|
1651
|
-
export class ClearMintedApiKeyAction extends
|
|
1757
|
+
export class ClearMintedApiKeyAction extends ApiKeysAction {
|
|
1652
1758
|
apply(s) { if (s.get().minted) s.set({ minted: null }); }
|
|
1653
1759
|
}
|
|
1654
1760
|
|
|
@@ -1696,8 +1802,10 @@ export class SwitchCollectionAction extends Action {
|
|
|
1696
1802
|
(id) => new ExecCommandAction('collections.switch', id),
|
|
1697
1803
|
() => new ExecCommandAction('collections.create'));
|
|
1698
1804
|
if (!items.length) return notifications.info('This drive has one collection.');
|
|
1699
|
-
|
|
1700
|
-
|
|
1805
|
+
// No geometry: this comes from a command, not from a click, so the overlay centres it.
|
|
1806
|
+
// The arithmetic that used to be here needed `window` and carried a `typeof window`
|
|
1807
|
+
// guard precisely because layout has no business in the bl layer.
|
|
1808
|
+
engine.dispatch(new ShowContextMenuAction(items));
|
|
1701
1809
|
}
|
|
1702
1810
|
}
|
|
1703
1811
|
|
|
@@ -1722,9 +1830,10 @@ export class ToggleDetailsAction extends Action {
|
|
|
1722
1830
|
|
|
1723
1831
|
/** Ask for a .zip and take it through the install review. */
|
|
1724
1832
|
export class PickAndInstallPluginAction extends Action {
|
|
1725
|
-
static deps = ['notifications', 'plugins', 'social', 'workbench'];
|
|
1833
|
+
static deps = ['notifications', 'overlay', 'plugins', 'social', 'workbench'];
|
|
1726
1834
|
async execute(r) {
|
|
1727
|
-
|
|
1835
|
+
const file = await pickZip();
|
|
1836
|
+
if (file) await beginInstallFromFile(r, file);
|
|
1728
1837
|
}
|
|
1729
1838
|
}
|
|
1730
1839
|
|
|
@@ -1760,7 +1869,7 @@ export class ClearSidecarAction extends Action {
|
|
|
1760
1869
|
static deps = ['social'];
|
|
1761
1870
|
constructor(nodeId) { super(); this.nodeId = nodeId; }
|
|
1762
1871
|
async execute({ social }) {
|
|
1763
|
-
if (social.
|
|
1872
|
+
if (social.get().sidecar?.nodeId === this.nodeId) social.loadSidecar(null);
|
|
1764
1873
|
}
|
|
1765
1874
|
}
|
|
1766
1875
|
|
|
@@ -1826,17 +1935,30 @@ export class SetGrantAction extends Action {
|
|
|
1826
1935
|
notifications.error?.(err?.message || 'Could not change access');
|
|
1827
1936
|
} finally {
|
|
1828
1937
|
acl.set({ busy: false });
|
|
1829
|
-
await engine
|
|
1938
|
+
await runAction(engine, new LoadGrantsAction(this.collectionId));
|
|
1830
1939
|
}
|
|
1831
1940
|
}
|
|
1832
1941
|
}
|
|
1833
1942
|
|
|
1834
1943
|
export class LoadRotationAction extends Action {
|
|
1835
1944
|
static deps = ['api', 'explorer', 'notifications', 'rotation'];
|
|
1945
|
+
|
|
1946
|
+
/** @param {string} [collectionId] which collection; defaults to the open one */
|
|
1947
|
+
constructor(collectionId = null) { super(); this.collectionId = collectionId; }
|
|
1948
|
+
|
|
1836
1949
|
async execute({ api, explorer, notifications, rotation }) {
|
|
1837
|
-
const collectionId = explorer.get().collectionId;
|
|
1950
|
+
const collectionId = this.collectionId || explorer.get().collectionId;
|
|
1838
1951
|
if (!collectionId) return;
|
|
1839
|
-
|
|
1952
|
+
// CLEARED when the collection changes, not merely re-stamped. `rotation` is one slice
|
|
1953
|
+
// for whichever collection the settings screen is looking at, and stamping the new id
|
|
1954
|
+
// while leaving the previous collection's `rotation` and `estimate` in place meant a
|
|
1955
|
+
// failed load — which sets only `{loading, error}` — left collection A's running
|
|
1956
|
+
// progress on screen under B's fingerprint, indefinitely, with a Start/Stop button
|
|
1957
|
+
// beside it.
|
|
1958
|
+
const switching = rotation.get().collectionId !== collectionId;
|
|
1959
|
+
rotation.set(switching
|
|
1960
|
+
? { collectionId, rotation: null, estimate: null, loading: true, error: null }
|
|
1961
|
+
: { collectionId, loading: true, error: null });
|
|
1840
1962
|
try {
|
|
1841
1963
|
// Both together: the estimate is meaningless without knowing whether one is already
|
|
1842
1964
|
// running, and a screen showing one without the other invites starting a second.
|
|
@@ -1868,7 +1990,7 @@ export class BeginRotationAction extends Action {
|
|
|
1868
1990
|
// fingerprint on screen is already out of date. Reload the collections or it keeps
|
|
1869
1991
|
// showing the key the collection is moving OFF, which is the one thing this field
|
|
1870
1992
|
// exists to get right.
|
|
1871
|
-
|
|
1993
|
+
engine.dispatch(new LoadCollectionsAction());
|
|
1872
1994
|
notifications.info('Key rotation started. It runs in the background and survives a reload.');
|
|
1873
1995
|
} catch (err) {
|
|
1874
1996
|
rotation.set({ busy: false });
|