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