@k2b/cloud 0.28.0 → 0.29.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/package.json +7 -5
- package/src/_internal/cli-plugins.ts +18 -0
- package/src/_internal/define-app.ts +21 -5
- package/src/_internal/registry-validation.ts +30 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.tsx +11 -4
- package/src/ai/assistant-models.ts +25 -4
- package/src/ai/audio-tool.ts +3 -10
- package/src/ai/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +97 -17
- package/src/ai/chat/builtin-tools.tsx +3 -1
- package/src/ai/chat/composer-adapter.ts +8 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +12 -3
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +28 -6
- package/src/ai/chat/messages.ts +16 -0
- package/src/ai/chat/presentation.tsx +1 -8
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-layout.ts +12 -0
- package/src/ai/chat/turn-view.tsx +15 -7
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/code-mode-skill.ts +4 -4
- package/src/ai/compaction.ts +22 -3
- package/src/ai/default-tools.ts +9 -2
- package/src/ai/executor.ts +127 -40
- package/src/ai/file-media-type.ts +4 -0
- package/src/ai/files-store.ts +17 -10
- package/src/ai/http.ts +11 -10
- package/src/ai/index.ts +1 -0
- package/src/ai/memories.ts +2 -0
- package/src/ai/memory-learning.ts +59 -5
- package/src/ai/memory-workflow-evidence.ts +35 -10
- package/src/ai/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +34 -1
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +20 -10
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +11 -3
- package/src/ai/store.ts +56 -20
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +5 -1
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +5 -1
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/types.ts +1 -0
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +71 -295
- package/src/browser/FileSaver.tsx +407 -0
- package/src/browser/app-badges.ts +16 -0
- package/src/browser/choose-files.browser-harness.tsx +22 -6
- package/src/browser/choose-files.tsx +23 -36
- package/src/browser/file-chooser-messages.ts +64 -0
- package/src/browser/file-providers.ts +258 -8
- package/src/browser/files.tsx +26 -3
- package/src/browser/notifications.ts +37 -1
- package/src/browser/provider-folder.tsx +301 -0
- package/src/browser/provider-list.ts +39 -0
- package/src/browser/save-files.browser-harness.tsx +35 -0
- package/src/browser/save-files.tsx +107 -0
- package/src/contracts/app.ts +10 -0
- package/src/contracts/commands.ts +3 -1
- package/src/contracts/notification-group.ts +6 -0
- package/src/contracts/notification-types.ts +6 -0
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/user-notifications.ts +74 -0
- package/src/server/middleware/request-logger.ts +11 -2
- package/src/services/accounts/avatar.ts +5 -2
- package/src/services/image-metadata.ts +438 -0
- package/src/services/index.ts +1 -0
- package/src/services/logging/redaction.ts +50 -0
- package/src/services/mandates/index.ts +4 -2
- package/src/services/mandates/policy.ts +20 -0
- package/src/services/notifications/browser.ts +48 -15
- package/src/services/notifications/dispatcher.ts +31 -1
- package/src/services/notifications/platform.ts +32 -4
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/user.ts +6 -0
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +2 -2
- package/src/shared/redirect.ts +2 -1
- package/src/shared/template-rendering.ts +120 -7
- package/src/ssr/AppBadge.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +47 -26
- package/src/ssr/IslandError.tsx +33 -0
- package/src/ssr/Layout.tsx +3 -0
- package/src/ssr/LayoutHelp.tsx +4 -2
- package/src/ssr/RailApps.island.tsx +9 -2
- package/src/ssr/app-badges.ts +93 -0
- package/src/ssr/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +23 -0
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/utilities-navigation.css +41 -0
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { fileIcons, text } from "@k2b/stdlib";
|
|
2
|
-
import { timed } from "@k2b/stdlib/solid";
|
|
3
2
|
import {
|
|
4
|
-
announce,
|
|
5
3
|
Button,
|
|
6
4
|
createCollectionSelection,
|
|
7
5
|
FileDropTarget,
|
|
8
6
|
FileGrid,
|
|
9
7
|
Format,
|
|
10
|
-
IconButton,
|
|
11
8
|
PanelDialog,
|
|
12
9
|
Placeholder,
|
|
13
10
|
ProgressBar,
|
|
14
11
|
StatusBadge,
|
|
15
12
|
type StatusTone,
|
|
16
|
-
TextInput,
|
|
17
13
|
useLocale,
|
|
18
14
|
} from "@k2b/ui";
|
|
19
15
|
import { batch, createMemo, createSignal, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js";
|
|
@@ -28,10 +24,10 @@ import {
|
|
|
28
24
|
type FileProviderCaller,
|
|
29
25
|
FileProviderError,
|
|
30
26
|
type FileProviderSource,
|
|
31
|
-
listProviderFolder,
|
|
32
27
|
providerIcon,
|
|
33
28
|
readProviderFile,
|
|
34
29
|
} from "./file-providers";
|
|
30
|
+
import { createProviderFolder, FolderContents, FolderToolbar } from "./provider-folder";
|
|
35
31
|
|
|
36
32
|
export type ChooseFilesOptions = {
|
|
37
33
|
/** `<input accept>` syntax, for example `"image/*,.pdf"`. */
|
|
@@ -50,8 +46,6 @@ export type ChooseFilesOptions = {
|
|
|
50
46
|
export type FileProviderList = { state: "loading" } | { state: "ready"; providers: readonly FileProviderSource[] } | { state: "error" };
|
|
51
47
|
|
|
52
48
|
type FileEntry = Extract<FileProviderEntry, { kind: "file" }>;
|
|
53
|
-
type Crumb = { id?: string; name: string };
|
|
54
|
-
type Location = { provider: FileProviderSource; trail: readonly Crumb[] } | null;
|
|
55
49
|
type SourceRow = { id: string; name: string; icon: string; provider?: FileProviderSource };
|
|
56
50
|
type Download = {
|
|
57
51
|
entry: FileEntry;
|
|
@@ -63,9 +57,6 @@ type Download = {
|
|
|
63
57
|
reason?: EntryProblem;
|
|
64
58
|
};
|
|
65
59
|
|
|
66
|
-
/** Empty pages may still continue; the chooser follows at most this many of them before it shows "Load more". */
|
|
67
|
-
const EMPTY_PAGES_FOLLOWED = 5;
|
|
68
|
-
|
|
69
60
|
const TAG_TONES: Record<NonNullable<FileProviderTag["tone"]>, StatusTone> = {
|
|
70
61
|
neutral: "neutral",
|
|
71
62
|
info: "info",
|
|
@@ -74,8 +65,8 @@ const TAG_TONES: Record<NonNullable<FileProviderTag["tone"]>, StatusTone> = {
|
|
|
74
65
|
danger: "error",
|
|
75
66
|
};
|
|
76
67
|
|
|
77
|
-
const localTimeZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
78
|
-
const entryIcon = (entry: FileProviderEntry) =>
|
|
68
|
+
export const localTimeZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
69
|
+
export const entryIcon = (entry: FileProviderEntry) =>
|
|
79
70
|
providerIcon(entry.icon, `ti ${fileIcons.getFileIcon({ name: entry.name, type: entry.kind === "folder" ? "directory" : "file" })}`);
|
|
80
71
|
|
|
81
72
|
/**
|
|
@@ -95,21 +86,21 @@ export function FileChooser(props: {
|
|
|
95
86
|
let deviceInput: HTMLInputElement | undefined;
|
|
96
87
|
let root: HTMLDivElement | undefined;
|
|
97
88
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
/** After navigation the activated row is gone; give focus to the first row so the keyboard continues there. */
|
|
90
|
+
const refocus = () => {
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
const active = document.activeElement;
|
|
93
|
+
if (active && root?.contains(active) && active.isConnected && active.tagName !== "DIALOG") return;
|
|
94
|
+
const first = location() ? items()[0]?.id : sources()[0]?.id;
|
|
95
|
+
if (first) (location() ? selection : sourceSelection).focus(first);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const folder = createProviderFolder({ caller: props.caller, t, settled: refocus });
|
|
99
|
+
const { location, items, openFolder } = folder;
|
|
105
100
|
const [downloads, setDownloads] = createStore<Download[]>([]);
|
|
106
101
|
const [downloading, setDownloading] = createSignal(false);
|
|
107
|
-
let request: AbortController | undefined;
|
|
108
102
|
let transfer: AbortController | undefined;
|
|
109
|
-
onCleanup(() =>
|
|
110
|
-
request?.abort();
|
|
111
|
-
transfer?.abort();
|
|
112
|
-
});
|
|
103
|
+
onCleanup(() => transfer?.abort());
|
|
113
104
|
|
|
114
105
|
const budget = props.options.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
115
106
|
/** The largest single file: the consumer's budget or the provider's read limit, whichever is smaller. */
|
|
@@ -127,7 +118,7 @@ export function FileChooser(props: {
|
|
|
127
118
|
const sources = createMemo<SourceRow[]>(() => {
|
|
128
119
|
const list = props.providers();
|
|
129
120
|
const rows = [device(), ...(list.state === "ready" ? list.providers.map(providerRow) : [])];
|
|
130
|
-
const needle = filter().trim().toLocaleLowerCase();
|
|
121
|
+
const needle = folder.filter().trim().toLocaleLowerCase();
|
|
131
122
|
return needle ? rows.filter((row) => row.name.toLocaleLowerCase().includes(needle)) : rows;
|
|
132
123
|
});
|
|
133
124
|
// Sources open on activation and keep no selection, so Escape always closes the chooser from here.
|
|
@@ -145,106 +136,11 @@ export function FileChooser(props: {
|
|
|
145
136
|
);
|
|
146
137
|
const overBudget = () => chosen().reduce((sum, entry) => sum + entry.size, 0) > budget;
|
|
147
138
|
|
|
148
|
-
const folderName = () => location()?.trail.at(-1)?.name ?? t().sources;
|
|
149
|
-
|
|
150
|
-
const load = async (more = false) => {
|
|
151
|
-
const current = location();
|
|
152
|
-
if (!current) return;
|
|
153
|
-
request?.abort();
|
|
154
|
-
const pending = new AbortController();
|
|
155
|
-
request = pending;
|
|
156
|
-
if (!more) {
|
|
157
|
-
batch(() => {
|
|
158
|
-
setItems([]);
|
|
159
|
-
setNext(null);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
setStatus(more ? "more" : "loading");
|
|
163
|
-
setError(null);
|
|
164
|
-
try {
|
|
165
|
-
let cursor = more ? (next() ?? undefined) : undefined;
|
|
166
|
-
let followed = 0;
|
|
167
|
-
for (;;) {
|
|
168
|
-
const page = await listProviderFolder(
|
|
169
|
-
current.provider,
|
|
170
|
-
{ parent: current.trail.at(-1)?.id, query: query() || undefined, cursor },
|
|
171
|
-
props.caller(),
|
|
172
|
-
pending.signal,
|
|
173
|
-
);
|
|
174
|
-
if (pending.signal.aborted) return;
|
|
175
|
-
batch(() => {
|
|
176
|
-
setItems((previous) => [...previous, ...page.items]);
|
|
177
|
-
setNext(page.next);
|
|
178
|
-
});
|
|
179
|
-
if (items().length > 0 || !page.next || ++followed >= EMPTY_PAGES_FOLLOWED) break;
|
|
180
|
-
cursor = page.next;
|
|
181
|
-
}
|
|
182
|
-
setStatus("ready");
|
|
183
|
-
if (!more) {
|
|
184
|
-
announce(t().folderAnnounced({ folder: folderName(), count: items().length, more: Boolean(next()) }));
|
|
185
|
-
refocus();
|
|
186
|
-
}
|
|
187
|
-
} catch (cause) {
|
|
188
|
-
if (pending.signal.aborted) return;
|
|
189
|
-
setError(
|
|
190
|
-
cause instanceof FileProviderError ? cause : new FileProviderError("INTERNAL", cause instanceof Error ? cause.message : "", 500),
|
|
191
|
-
);
|
|
192
|
-
setStatus(more ? "ready" : "error");
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
/** After navigation the activated row is gone; give focus to the first row so the keyboard continues there. */
|
|
197
|
-
const refocus = () => {
|
|
198
|
-
queueMicrotask(() => {
|
|
199
|
-
const active = document.activeElement;
|
|
200
|
-
if (active && root?.contains(active) && active.isConnected && active.tagName !== "DIALOG") return;
|
|
201
|
-
const first = location() ? items()[0]?.id : sources()[0]?.id;
|
|
202
|
-
if (first) (location() ? selection : sourceSelection).focus(first);
|
|
203
|
-
});
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const applyQuery = (value: string) => {
|
|
207
|
-
setQuery(value.trim());
|
|
208
|
-
void load();
|
|
209
|
-
};
|
|
210
|
-
const { debouncedFn: applyFilter, cancel: cancelFilter } = timed.debounce(applyQuery, 250);
|
|
211
|
-
const clearFilter = () => {
|
|
212
|
-
cancelFilter();
|
|
213
|
-
setFilter("");
|
|
214
|
-
if (location() && query()) applyQuery("");
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const navigate = (next: Location) => {
|
|
218
|
-
cancelFilter();
|
|
219
|
-
request?.abort();
|
|
220
|
-
batch(() => {
|
|
221
|
-
setFilter("");
|
|
222
|
-
setQuery("");
|
|
223
|
-
setLocation(next);
|
|
224
|
-
setItems([]);
|
|
225
|
-
setNext(null);
|
|
226
|
-
setError(null);
|
|
227
|
-
setStatus(next ? "loading" : "ready");
|
|
228
|
-
});
|
|
229
|
-
if (next) void load();
|
|
230
|
-
else refocus();
|
|
231
|
-
};
|
|
232
|
-
const openProvider = (provider: FileProviderSource) => navigate({ provider, trail: [{ name: provider.name }] });
|
|
233
|
-
const openFolder = (entry: FileProviderEntry) => {
|
|
234
|
-
const current = location();
|
|
235
|
-
if (current) navigate({ provider: current.provider, trail: [...current.trail, { id: entry.id, name: entry.name }] });
|
|
236
|
-
};
|
|
237
|
-
const up = () => {
|
|
238
|
-
const current = location();
|
|
239
|
-
if (!current) return;
|
|
240
|
-
navigate(current.trail.length > 1 ? { provider: current.provider, trail: current.trail.slice(0, -1) } : null);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
139
|
const openSource = (row: SourceRow) => {
|
|
244
140
|
sourceSelection.clear();
|
|
245
141
|
// A click or key press on the row is the user activation the native picker needs.
|
|
246
142
|
if (!row.provider) deviceInput?.click();
|
|
247
|
-
else openProvider(row.provider);
|
|
143
|
+
else folder.openProvider(row.provider);
|
|
248
144
|
};
|
|
249
145
|
|
|
250
146
|
const add = async () => {
|
|
@@ -302,18 +198,6 @@ export function FileChooser(props: {
|
|
|
302
198
|
const ready = () => downloads.filter((item) => item.state === "done").length;
|
|
303
199
|
const progress = () => t().downloading({ done: ready(), total: downloads.length, failed: failures() });
|
|
304
200
|
|
|
305
|
-
const errorView = createMemo(() => {
|
|
306
|
-
const value = error();
|
|
307
|
-
if (!value) return null;
|
|
308
|
-
const app = location()?.provider.name ?? "";
|
|
309
|
-
if (value.status === 0) return { title: t().offline, description: t().offlineHint, retry: true };
|
|
310
|
-
if (value.status === 403) return { title: t().noAccess, description: t().backHint, retry: false };
|
|
311
|
-
if (value.status === 404) return { title: t().notFound, description: t().backHint, retry: false };
|
|
312
|
-
if (value.status >= 502 || value.code === "APP_UNAVAILABLE")
|
|
313
|
-
return { title: t().unavailable({ app }), description: t().unavailableHint, retry: true };
|
|
314
|
-
return { title: t().loadFailed, description: value.message || undefined, retry: true };
|
|
315
|
-
});
|
|
316
|
-
|
|
317
201
|
const renderTags = (tags: readonly FileProviderTag[] | undefined) => (
|
|
318
202
|
<For each={tags ?? []}>
|
|
319
203
|
{(tag) => <StatusBadge tone={TAG_TONES[tag.tone ?? "neutral"]} icon={null} label={tag.label} class="cloud-file-chooser__tag" />}
|
|
@@ -335,64 +219,7 @@ export function FileChooser(props: {
|
|
|
335
219
|
if (files.length > 0) props.close(files);
|
|
336
220
|
}}
|
|
337
221
|
/>
|
|
338
|
-
<
|
|
339
|
-
<nav class="cloud-file-chooser__crumbs" aria-label={t().breadcrumbs}>
|
|
340
|
-
<IconButton size="sm" variant="ghost" label={t().up} disabled={!location() || downloading()} onClick={up}>
|
|
341
|
-
<i class="ti ti-arrow-up" aria-hidden="true" />
|
|
342
|
-
</IconButton>
|
|
343
|
-
<ol>
|
|
344
|
-
<li>
|
|
345
|
-
<Button
|
|
346
|
-
size="sm"
|
|
347
|
-
variant={location() ? "text" : "subtle"}
|
|
348
|
-
aria-current={location() ? undefined : "location"}
|
|
349
|
-
disabled={downloading()}
|
|
350
|
-
onClick={() => navigate(null)}
|
|
351
|
-
>
|
|
352
|
-
{t().sources}
|
|
353
|
-
</Button>
|
|
354
|
-
</li>
|
|
355
|
-
<For each={location()?.trail ?? []}>
|
|
356
|
-
{(crumb, index) => {
|
|
357
|
-
const last = () => index() === (location()?.trail.length ?? 0) - 1;
|
|
358
|
-
return (
|
|
359
|
-
<li>
|
|
360
|
-
<span aria-hidden="true">/</span>
|
|
361
|
-
<Button
|
|
362
|
-
size="sm"
|
|
363
|
-
variant={last() ? "subtle" : "text"}
|
|
364
|
-
aria-current={last() ? "location" : undefined}
|
|
365
|
-
disabled={downloading()}
|
|
366
|
-
onClick={() => {
|
|
367
|
-
const current = location();
|
|
368
|
-
if (current && !last()) navigate({ provider: current.provider, trail: current.trail.slice(0, index() + 1) });
|
|
369
|
-
}}
|
|
370
|
-
>
|
|
371
|
-
{crumb.name}
|
|
372
|
-
</Button>
|
|
373
|
-
</li>
|
|
374
|
-
);
|
|
375
|
-
}}
|
|
376
|
-
</For>
|
|
377
|
-
</ol>
|
|
378
|
-
</nav>
|
|
379
|
-
<TextInput
|
|
380
|
-
class="cloud-file-chooser__filter"
|
|
381
|
-
type="search"
|
|
382
|
-
icon="ti ti-filter"
|
|
383
|
-
aria-label={t().filter}
|
|
384
|
-
placeholder={t().filter}
|
|
385
|
-
maxLength={200}
|
|
386
|
-
disabled={downloading()}
|
|
387
|
-
value={filter}
|
|
388
|
-
onValueChange={(value) => {
|
|
389
|
-
setFilter(value);
|
|
390
|
-
if (location()) applyFilter(value);
|
|
391
|
-
}}
|
|
392
|
-
clearable
|
|
393
|
-
onClear={clearFilter}
|
|
394
|
-
/>
|
|
395
|
-
</div>
|
|
222
|
+
<FolderToolbar folder={folder} t={t} disabled={downloading()} sources />
|
|
396
223
|
<PanelDialog.Body>
|
|
397
224
|
<input
|
|
398
225
|
ref={deviceInput}
|
|
@@ -469,113 +296,62 @@ export function FileChooser(props: {
|
|
|
469
296
|
</Match>
|
|
470
297
|
</Switch>
|
|
471
298
|
</Match>
|
|
472
|
-
<Match when={
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
299
|
+
<Match when={true}>
|
|
300
|
+
<FolderContents folder={folder} t={t}>
|
|
301
|
+
<FileGrid
|
|
302
|
+
rows={items()}
|
|
303
|
+
getRowId={(entry) => entry.id}
|
|
304
|
+
selection={selection}
|
|
305
|
+
label={t().entries({ folder: folder.folderName() })}
|
|
306
|
+
layout="list"
|
|
307
|
+
renderPreview={(entry) => (
|
|
308
|
+
<i
|
|
309
|
+
class={`${selection.selected().has(entry.id) && entry.kind === "file" ? "ti ti-circle-check" : entryIcon(entry)} cloud-file-chooser__icon`}
|
|
310
|
+
data-selected={selection.selected().has(entry.id) && entry.kind === "file" ? "true" : undefined}
|
|
311
|
+
aria-hidden="true"
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
renderLabel={(entry) => <span title={entry.name}>{entry.name}</span>}
|
|
315
|
+
renderMeta={(entry) =>
|
|
316
|
+
entry.kind === "folder" && !entry.tags?.length ? null : (
|
|
317
|
+
<span class="cloud-file-chooser__meta">
|
|
318
|
+
<Switch>
|
|
319
|
+
<Match when={entry.kind === "file" && problem(entry) === "size"}>
|
|
320
|
+
<span>{t().tooLarge({ limit: text.pprintBytes(limit(), { locale: locale() }) })}</span>
|
|
321
|
+
</Match>
|
|
322
|
+
<Match when={entry.kind === "file" && problem(entry) === "type"}>
|
|
323
|
+
<span>{t().wrongType}</span>
|
|
324
|
+
</Match>
|
|
325
|
+
<Match when={entry.kind === "file" ? entry : undefined}>
|
|
326
|
+
{(file) => (
|
|
327
|
+
<>
|
|
328
|
+
<Format.Bytes value={file().size} />
|
|
329
|
+
<Show when={file().updatedAt}>
|
|
330
|
+
{(updated) => (
|
|
331
|
+
<>
|
|
332
|
+
<span aria-hidden="true">·</span>
|
|
333
|
+
<Format.Date value={updated()} timeZone={localTimeZone()} />
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
</Show>
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
</Match>
|
|
340
|
+
</Switch>
|
|
341
|
+
{renderTags(entry.tags)}
|
|
342
|
+
</span>
|
|
492
343
|
)
|
|
493
344
|
}
|
|
345
|
+
onRowClick={(entry) => {
|
|
346
|
+
if (entry.kind === "folder") openFolder(entry);
|
|
347
|
+
}}
|
|
348
|
+
onOpen={(entry) => {
|
|
349
|
+
if (entry.kind === "folder") return openFolder(entry);
|
|
350
|
+
if (!selection.selected().has(entry.id)) selection.toggle(entry.id);
|
|
351
|
+
void add();
|
|
352
|
+
}}
|
|
494
353
|
/>
|
|
495
|
-
|
|
496
|
-
</Match>
|
|
497
|
-
<Match when={items().length === 0 && !next()}>
|
|
498
|
-
<Show
|
|
499
|
-
when={query()}
|
|
500
|
-
fallback={<Placeholder variant="panel" class="cloud-file-chooser__state" icon="ti ti-folder" title={t().emptyFolder} />}
|
|
501
|
-
>
|
|
502
|
-
<Placeholder
|
|
503
|
-
variant="panel"
|
|
504
|
-
class="cloud-file-chooser__state"
|
|
505
|
-
icon="ti ti-filter-off"
|
|
506
|
-
title={t().noMatches({ query: query() })}
|
|
507
|
-
action={
|
|
508
|
-
<Button size="sm" variant="secondary" onClick={clearFilter}>
|
|
509
|
-
{t().clearFilter}
|
|
510
|
-
</Button>
|
|
511
|
-
}
|
|
512
|
-
/>
|
|
513
|
-
</Show>
|
|
514
|
-
</Match>
|
|
515
|
-
<Match when={true}>
|
|
516
|
-
<FileGrid
|
|
517
|
-
rows={items()}
|
|
518
|
-
getRowId={(entry) => entry.id}
|
|
519
|
-
selection={selection}
|
|
520
|
-
label={t().entries({ folder: folderName() })}
|
|
521
|
-
layout="list"
|
|
522
|
-
renderPreview={(entry) => (
|
|
523
|
-
<i
|
|
524
|
-
class={`${selection.selected().has(entry.id) && entry.kind === "file" ? "ti ti-circle-check" : entryIcon(entry)} cloud-file-chooser__icon`}
|
|
525
|
-
data-selected={selection.selected().has(entry.id) && entry.kind === "file" ? "true" : undefined}
|
|
526
|
-
aria-hidden="true"
|
|
527
|
-
/>
|
|
528
|
-
)}
|
|
529
|
-
renderLabel={(entry) => <span title={entry.name}>{entry.name}</span>}
|
|
530
|
-
renderMeta={(entry) =>
|
|
531
|
-
entry.kind === "folder" && !entry.tags?.length ? null : (
|
|
532
|
-
<span class="cloud-file-chooser__meta">
|
|
533
|
-
<Switch>
|
|
534
|
-
<Match when={entry.kind === "file" && problem(entry) === "size"}>
|
|
535
|
-
<span>{t().tooLarge({ limit: text.pprintBytes(limit(), { locale: locale() }) })}</span>
|
|
536
|
-
</Match>
|
|
537
|
-
<Match when={entry.kind === "file" && problem(entry) === "type"}>
|
|
538
|
-
<span>{t().wrongType}</span>
|
|
539
|
-
</Match>
|
|
540
|
-
<Match when={entry.kind === "file" ? entry : undefined}>
|
|
541
|
-
{(file) => (
|
|
542
|
-
<>
|
|
543
|
-
<Format.Bytes value={file().size} />
|
|
544
|
-
<Show when={file().updatedAt}>
|
|
545
|
-
{(updated) => (
|
|
546
|
-
<>
|
|
547
|
-
<span aria-hidden="true">·</span>
|
|
548
|
-
<Format.Date value={updated()} timeZone={localTimeZone()} />
|
|
549
|
-
</>
|
|
550
|
-
)}
|
|
551
|
-
</Show>
|
|
552
|
-
</>
|
|
553
|
-
)}
|
|
554
|
-
</Match>
|
|
555
|
-
</Switch>
|
|
556
|
-
{renderTags(entry.tags)}
|
|
557
|
-
</span>
|
|
558
|
-
)
|
|
559
|
-
}
|
|
560
|
-
onRowClick={(entry) => {
|
|
561
|
-
if (entry.kind === "folder") openFolder(entry);
|
|
562
|
-
}}
|
|
563
|
-
onOpen={(entry) => {
|
|
564
|
-
if (entry.kind === "folder") return openFolder(entry);
|
|
565
|
-
if (!selection.selected().has(entry.id)) selection.toggle(entry.id);
|
|
566
|
-
void add();
|
|
567
|
-
}}
|
|
568
|
-
/>
|
|
569
|
-
<div class="cloud-file-chooser__more">
|
|
570
|
-
<Show when={error() && status() === "ready"}>
|
|
571
|
-
<Placeholder state="error" variant="inline" align="left" description={errorView()?.title} />
|
|
572
|
-
</Show>
|
|
573
|
-
<Show when={next()}>
|
|
574
|
-
<Button variant="secondary" size="sm" loading={status() === "more"} onClick={() => void load(true)}>
|
|
575
|
-
{error() ? t().retry : t().loadMore}
|
|
576
|
-
</Button>
|
|
577
|
-
</Show>
|
|
578
|
-
</div>
|
|
354
|
+
</FolderContents>
|
|
579
355
|
</Match>
|
|
580
356
|
</Switch>
|
|
581
357
|
</PanelDialog.Body>
|