@deckbuilderkit/view-explorer 0.0.0-stage → 0.1.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/README.md +12 -2
- package/package.json +40 -4
- package/src/ExplorerScreen.tsx +742 -0
- package/src/client.ts +104 -0
- package/src/commands.ts +29 -0
- package/src/explorer/CompareTab.tsx +52 -0
- package/src/explorer/CurlPanel.tsx +144 -0
- package/src/explorer/DetailsTab.tsx +284 -0
- package/src/explorer/FileTab.tsx +466 -0
- package/src/explorer/LogTab.tsx +130 -0
- package/src/explorer/ProjectSearch.tsx +117 -0
- package/src/explorer/ProjectTree.tsx +824 -0
- package/src/explorer/PropertiesDialog.tsx +144 -0
- package/src/explorer/SqlPanel.tsx +221 -0
- package/src/explorer/TreePane.tsx +216 -0
- package/src/explorer/copy-path.ts +9 -0
- package/src/explorer/file-undo.ts +36 -0
- package/src/explorer/fs-actions.ts +34 -0
- package/src/explorer/name-index.ts +58 -0
- package/src/explorer/os-drop.ts +56 -0
- package/src/explorer/resolve-image-path.ts +19 -0
- package/src/explorer/tab-actions.ts +17 -0
- package/src/explorer/tree-find.ts +37 -0
- package/src/explorer/useLocalImages.ts +49 -0
- package/src/explorer-paths.ts +9 -0
- package/src/explorer-state.ts +21 -0
- package/src/explorer.css +61 -0
- package/src/i18n.ts +526 -0
- package/src/options.ts +8 -0
- package/src/server.ts +156 -0
- package/src/settings/cards.tsx +99 -0
- package/src/settings.ts +107 -0
- package/src/view.tsx +38 -0
- package/src/web.ts +4 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { openWithDefault, revealInFolder } from '@deckbuilderkit/platform';
|
|
3
|
+
import { defineServerView, invalid, isRecord, type ServerViewDef } from '@deckbuilderkit/server-core';
|
|
4
|
+
import { dialogRoutes, editorRoutes, fromPlatform, pickedPaths, recordRoutes, rootedFs, type RootedFs } from '@deckbuilderkit/server-mods';
|
|
5
|
+
import type { ExplorerSettings } from './explorer-state';
|
|
6
|
+
|
|
7
|
+
/** The part of the server settings the Explorer view owns; the app's `Settings` extends it. */
|
|
8
|
+
export interface ExplorerServerSettings {
|
|
9
|
+
/** Explorer screen state; checked by `validExplorer`. */
|
|
10
|
+
explorer?: ExplorerSettings;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MARKDOWN_MODES: unknown[] = ['live', 'edit', 'split', 'preview'];
|
|
14
|
+
const MAX_TABS_LAYOUT_BYTES = 64 * 1024;
|
|
15
|
+
const MAX_EXPANDED_IDS = 2000;
|
|
16
|
+
const MAX_ID_LENGTH = 64;
|
|
17
|
+
const ID = /^[\w-]{1,64}$/;
|
|
18
|
+
|
|
19
|
+
const isStringArray = (v: unknown): v is string[] => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
20
|
+
|
|
21
|
+
const validPair = (v: unknown): [number, number] | null =>
|
|
22
|
+
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number' && n >= 0 && n <= 1) && Math.abs(v[0] + v[1] - 1) <= 0.01
|
|
23
|
+
? [v[0], v[1]]
|
|
24
|
+
: null;
|
|
25
|
+
|
|
26
|
+
/** A project-id to connection-id map, or null when malformed. */
|
|
27
|
+
function validConnections(raw: unknown): Record<string, string> | null {
|
|
28
|
+
if (!isRecord(raw)) return null;
|
|
29
|
+
const out: Record<string, string> = {};
|
|
30
|
+
for (const [project, id] of Object.entries(raw)) {
|
|
31
|
+
if (!ID.test(project) || typeof id !== 'string' || !ID.test(id)) return null;
|
|
32
|
+
out[project] = id;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A copy of `raw` when it is a well-formed explorer state within the size limits, else null. */
|
|
38
|
+
export function validExplorer(raw: unknown): ExplorerSettings | null {
|
|
39
|
+
const r = raw as Partial<ExplorerSettings> | null;
|
|
40
|
+
if (typeof r !== 'object' || r === null) return null;
|
|
41
|
+
const { sizes, collapsed, expanded, tabs, trees, treeSizes, markdownMode, wrapLines, sqlSizes, connections } = r;
|
|
42
|
+
if (!Array.isArray(sizes) || sizes.length !== 2 || !sizes.every((n) => typeof n === 'number' && n >= 0 && n <= 1)) return null;
|
|
43
|
+
if (Math.abs(sizes[0] + sizes[1] - 1) > 0.01) return null;
|
|
44
|
+
if (typeof collapsed !== 'boolean' || typeof expanded !== 'object' || expanded === null || Array.isArray(expanded)) return null;
|
|
45
|
+
const out: Record<string, string[]> = {};
|
|
46
|
+
for (const [project, ids] of Object.entries(expanded)) {
|
|
47
|
+
if (project.length > MAX_ID_LENGTH || !isStringArray(ids) || ids.length > MAX_EXPANDED_IDS) return null;
|
|
48
|
+
out[project] = ids;
|
|
49
|
+
}
|
|
50
|
+
if (tabs !== undefined && (typeof tabs !== 'object' || tabs === null || JSON.stringify(tabs).length >= MAX_TABS_LAYOUT_BYTES)) return null;
|
|
51
|
+
if (trees !== undefined && (!isStringArray(trees) || trees.length < 1 || trees.length > 2 || trees.some((id) => id.length > MAX_ID_LENGTH))) return null;
|
|
52
|
+
const pair = treeSizes === undefined ? undefined : validPair(treeSizes);
|
|
53
|
+
if (treeSizes !== undefined && !pair) return null;
|
|
54
|
+
if (markdownMode !== undefined && !MARKDOWN_MODES.includes(markdownMode)) return null;
|
|
55
|
+
if (wrapLines !== undefined && typeof wrapLines !== 'boolean') return null;
|
|
56
|
+
const sqlPair = sqlSizes === undefined ? undefined : validPair(sqlSizes);
|
|
57
|
+
if (sqlSizes !== undefined && !sqlPair) return null;
|
|
58
|
+
const conns = connections === undefined ? undefined : validConnections(connections);
|
|
59
|
+
if (connections !== undefined && !conns) return null;
|
|
60
|
+
return {
|
|
61
|
+
sizes: [sizes[0], sizes[1]],
|
|
62
|
+
collapsed,
|
|
63
|
+
expanded: out,
|
|
64
|
+
...(trees !== undefined && { trees }),
|
|
65
|
+
...(pair && { treeSizes: pair }),
|
|
66
|
+
...(markdownMode !== undefined && { markdownMode }),
|
|
67
|
+
...(wrapLines !== undefined && { wrapLines }),
|
|
68
|
+
...(sqlPair && { sqlSizes: sqlPair }),
|
|
69
|
+
...(conns && { connections: conns }),
|
|
70
|
+
...(tabs !== undefined && { tabs }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Stores = {
|
|
75
|
+
projects: Parameters<typeof recordRoutes>[0];
|
|
76
|
+
projectRoots: Parameters<typeof rootedFs>[0]['roots'];
|
|
77
|
+
pickedFolders: ReturnType<typeof pickedPaths>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const isDirectory = (path: string) => {
|
|
81
|
+
try {
|
|
82
|
+
return statSync(path).isDirectory();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface ExplorerServerOptions {
|
|
89
|
+
/** False: the delete route takes one path only. */
|
|
90
|
+
multiDelete?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ExplorerServerView = ServerViewDef<ExplorerServerSettings> & {
|
|
94
|
+
/** A project folder as a terminal's working directory; valid once the view is mounted. */
|
|
95
|
+
resolveCwd(root: string, path: string): Promise<string>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Server half of the Explorer view: the rooted file system routes, upload/download tickets, the
|
|
100
|
+
* project record routes, reveal/open, the editors and folder dialog routes, and the `explorer`
|
|
101
|
+
* settings group. Needs `ctx.stores.projects`, `projectRoots` and `pickedFolders`.
|
|
102
|
+
*/
|
|
103
|
+
export function explorerServerView(opts: ExplorerServerOptions = {}): ExplorerServerView {
|
|
104
|
+
let fs: RootedFs | undefined;
|
|
105
|
+
const mounted = () => {
|
|
106
|
+
if (!fs) throw new Error('The Explorer view is not mounted.');
|
|
107
|
+
return fs;
|
|
108
|
+
};
|
|
109
|
+
const view = defineServerView<ExplorerServerSettings>({
|
|
110
|
+
id: 'explorer',
|
|
111
|
+
routes(ctx) {
|
|
112
|
+
const stores = ctx.stores as unknown as Stores;
|
|
113
|
+
const rooted = rootedFs({ multiDelete: opts.multiDelete, roots: stores.projectRoots });
|
|
114
|
+
fs = rooted;
|
|
115
|
+
const target = async (json: <T>() => Promise<T>) => {
|
|
116
|
+
const { root, path } = await json<{ root?: unknown; path?: unknown }>();
|
|
117
|
+
return (await rooted.resolve(String(root), typeof path === 'string' ? path : '', { mustExist: true })).real;
|
|
118
|
+
};
|
|
119
|
+
void migrateConnections(ctx.settings);
|
|
120
|
+
return {
|
|
121
|
+
...recordRoutes(stores.projects, '/api/projects'),
|
|
122
|
+
...rooted.routes,
|
|
123
|
+
...dialogRoutes({ picked: stores.pickedFolders }),
|
|
124
|
+
...editorRoutes({ dir: ctx.dataDir, fs: rooted }),
|
|
125
|
+
'POST /api/fs/reveal': async ({ json }) => {
|
|
126
|
+
await revealInFolder(await target(json)).catch(fromPlatform);
|
|
127
|
+
return { ok: true };
|
|
128
|
+
},
|
|
129
|
+
'POST /api/fs/open': async ({ json }) => {
|
|
130
|
+
await openWithDefault(await target(json)).catch(fromPlatform);
|
|
131
|
+
return { ok: true };
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
unauthenticated: () => mounted().openRoutes,
|
|
136
|
+
settings: { validate: { explorer: validExplorer } },
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
...view,
|
|
140
|
+
async resolveCwd(root, path) {
|
|
141
|
+
const { real } = await mounted().resolve(root, path, { mustExist: true });
|
|
142
|
+
if (!isDirectory(real)) throw invalid('notADirectory', 'The path is not a folder.');
|
|
143
|
+
return real;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Carries the Workbench's old `explorerConnections` into `explorer.connections` when the latter is unset. */
|
|
149
|
+
async function migrateConnections(settings: { load(): Promise<ExplorerServerSettings & { workbench?: { explorerConnections?: unknown } }>; save?(patch: Record<string, unknown>): Promise<unknown> }): Promise<void> {
|
|
150
|
+
if (!settings.save) return;
|
|
151
|
+
const s = await settings.load();
|
|
152
|
+
const old = s.workbench?.explorerConnections;
|
|
153
|
+
if (!s.explorer || s.explorer.connections !== undefined || !old) return;
|
|
154
|
+
const connections = validConnections(old);
|
|
155
|
+
if (connections) await settings.save({ explorer: { ...s.explorer, connections } });
|
|
156
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { deepEqual, FILE_DEFAULTS, fileSettingsOf, SettingItem, type SettingsCardProps, type SettingsFieldProps, useResync } from '@deckbuilderkit/ui';
|
|
3
|
+
import { t } from '../i18n';
|
|
4
|
+
import type { ExplorerViewSettings } from '../settings';
|
|
5
|
+
|
|
6
|
+
function NestingField({ value, onChange }: { value: Record<string, string>; onChange: (next: Record<string, string>) => void }) {
|
|
7
|
+
const toText = (v: Record<string, string>) => Object.entries(v).map(([k, c]) => `${k}: ${c}`).join(String.fromCharCode(10));
|
|
8
|
+
const [text, setText] = useState(toText(value));
|
|
9
|
+
const area = useResync(value, () => setText(toText(value)));
|
|
10
|
+
return (
|
|
11
|
+
<SettingItem id="files.nestingPatterns" modified={!deepEqual(value, FILE_DEFAULTS.nestingPatterns)}>
|
|
12
|
+
<div className="field">
|
|
13
|
+
<span className="mono-label">{t('settings.explorer.nesting.patterns')}</span>
|
|
14
|
+
<textarea
|
|
15
|
+
ref={area}
|
|
16
|
+
value={text}
|
|
17
|
+
rows={6}
|
|
18
|
+
spellCheck={false}
|
|
19
|
+
aria-label={t('settings.explorer.nesting.patterns')}
|
|
20
|
+
onChange={(e) => setText(e.target.value)}
|
|
21
|
+
onBlur={() => {
|
|
22
|
+
const next: Record<string, string> = {};
|
|
23
|
+
for (const line of text.split(String.fromCharCode(10))) {
|
|
24
|
+
const at = line.indexOf(':');
|
|
25
|
+
const parent = line.slice(0, at).trim();
|
|
26
|
+
const children = line.slice(at + 1).trim();
|
|
27
|
+
if (at > 0 && parent && children) next[parent] = children;
|
|
28
|
+
}
|
|
29
|
+
onChange(next);
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
<span className="muted">{t('settings.explorer.nesting.patterns.hint')}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</SettingItem>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ExcludeField({ value, onChange }: { value: string[]; onChange: (next: string[]) => void }) {
|
|
39
|
+
const [text, setText] = useState(value.join('\n'));
|
|
40
|
+
const area = useResync(value, () => setText(value.join('\n')));
|
|
41
|
+
return (
|
|
42
|
+
<SettingItem id="files.exclude" modified={!deepEqual(value, FILE_DEFAULTS.exclude)}>
|
|
43
|
+
<div className="field">
|
|
44
|
+
<span className="mono-label">{t('settings.explorer.exclude')}</span>
|
|
45
|
+
<textarea
|
|
46
|
+
ref={area}
|
|
47
|
+
value={text}
|
|
48
|
+
rows={4}
|
|
49
|
+
spellCheck={false}
|
|
50
|
+
aria-label={t('settings.explorer.exclude')}
|
|
51
|
+
onChange={(e) => setText(e.target.value)}
|
|
52
|
+
onBlur={() => onChange(text.split('\n').map((l) => l.trim()).filter(Boolean))}
|
|
53
|
+
/>
|
|
54
|
+
<span className="muted">{t('settings.explorer.exclude.hint')}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</SettingItem>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The hand-written rows of the `files.nestingPatterns` and `files.exclude` settings (search results show them too). */
|
|
61
|
+
export function NestingSettingField({ settings, onChange }: SettingsFieldProps<ExplorerViewSettings>) {
|
|
62
|
+
const files = fileSettingsOf(settings.files);
|
|
63
|
+
return <NestingField value={files.nestingPatterns} onChange={(nestingPatterns) => onChange({ files: { ...files, nestingPatterns } })} />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ExcludeSettingField({ settings, onChange }: SettingsFieldProps<ExplorerViewSettings>) {
|
|
67
|
+
const files = fileSettingsOf(settings.files);
|
|
68
|
+
return <ExcludeField value={files.exclude} onChange={(exclude) => onChange({ files: { ...files, exclude } })} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How the Explorer tree and its tabs behave (`files.*` settings, shared with the file tabs). */
|
|
72
|
+
export function ExplorerCard({ settings, onChange, row }: SettingsCardProps<ExplorerViewSettings>) {
|
|
73
|
+
const files = fileSettingsOf(settings.files);
|
|
74
|
+
const set = (patch: Partial<typeof files>) => onChange({ files: { ...files, ...patch } });
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<section className="section">
|
|
78
|
+
<h4 className="mono-label">{t('settings.explorer.tabs')}</h4>
|
|
79
|
+
<div className="card form">
|
|
80
|
+
{['previewTabs', 'autoReveal', 'breadcrumbs', 'restoreTabs', 'markdownToc'].map((k) => row(`files.${k}`))}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
<section className="section">
|
|
84
|
+
<h4 className="mono-label">{t('settings.explorer.view')}</h4>
|
|
85
|
+
<div className="card form">
|
|
86
|
+
{row('files.compactFolders')}
|
|
87
|
+
{row('files.sortOrder')}
|
|
88
|
+
{row('files.fileNesting')}
|
|
89
|
+
{row('git.explorerDecorations')}
|
|
90
|
+
{row('files.showHidden')}
|
|
91
|
+
{row('files.confirmDelete')}
|
|
92
|
+
{row('files.confirmDragDrop')}
|
|
93
|
+
{files.fileNesting && <NestingField value={files.nestingPatterns} onChange={(nestingPatterns) => set({ nestingPatterns })} />}
|
|
94
|
+
<ExcludeField value={files.exclude} onChange={(exclude) => set({ exclude })} />
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { DEFAULT_FORMAT_OPTIONS, type SqlFormatOptions } from '@deckbuilderkit/editor/options';
|
|
2
|
+
import type { CopyFormat, SqlRunnerSettings } from '@deckbuilderkit/sql-ui';
|
|
3
|
+
import { DEFAULT_RESULT_FORMAT, FILE_DEFAULTS, fileBoolSetting, fileEnumSetting, fileSettingsOf, type FileSettings, type GitSettings, type LogSettings, type ResultFormat, type SectionPath, type SettingDef, SORT_ORDERS } from '@deckbuilderkit/ui';
|
|
4
|
+
|
|
5
|
+
import type { ExplorerSettings } from './explorer-state';
|
|
6
|
+
|
|
7
|
+
export type { ExplorerSettings };
|
|
8
|
+
|
|
9
|
+
/** The part of the Workbench state the Explorer reads (SQL files) and writes (`doc`, to open a file there). */
|
|
10
|
+
export interface ExplorerWorkbench {
|
|
11
|
+
/** Project ids allowed to run SQL from the Explorer. */
|
|
12
|
+
explorerProjects: string[];
|
|
13
|
+
maxRows: number;
|
|
14
|
+
results: Partial<ResultFormat>;
|
|
15
|
+
copyFormat: CopyFormat;
|
|
16
|
+
confirmDestructive: boolean;
|
|
17
|
+
queryTimeout: number;
|
|
18
|
+
format: Partial<SqlFormatOptions>;
|
|
19
|
+
doc: { kind: 'default' } | { kind: 'file'; project: string; path: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The part of the app settings the Explorer reads and writes; the app's `Settings` extends it. */
|
|
23
|
+
export interface ExplorerViewSettings {
|
|
24
|
+
/** State of the Explorer screen; the server validates it. */
|
|
25
|
+
explorer?: ExplorerSettings;
|
|
26
|
+
/** Log viewer: highlight words and bookmarks (keyed `<projectId>:<path>`). */
|
|
27
|
+
logs?: LogSettings;
|
|
28
|
+
/** How text files are saved and how the Explorer shows files. */
|
|
29
|
+
files?: FileSettings;
|
|
30
|
+
/** Read only: the editor's word wrap. */
|
|
31
|
+
editor?: { wordWrap?: boolean };
|
|
32
|
+
/** Read only: closing dirty tabs. */
|
|
33
|
+
general?: { confirmCloseDirty?: boolean };
|
|
34
|
+
/** Read only: the sidebar side of the app's window settings. */
|
|
35
|
+
window?: { sidebarSide?: 'left' | 'right' };
|
|
36
|
+
/** Read only: the REST view's active environment, whose variables `.curl` files use. */
|
|
37
|
+
rest?: { environment?: string };
|
|
38
|
+
/** Read only: the shared git group. */
|
|
39
|
+
git?: GitSettings;
|
|
40
|
+
/** The Workbench state, for SQL files. */
|
|
41
|
+
workbench?: Partial<ExplorerWorkbench>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const editorSettingsOf = (s: ExplorerViewSettings) => ({ wordWrap: s.editor?.wordWrap ?? true });
|
|
45
|
+
export const generalSettingsOf = (s: ExplorerViewSettings) => ({ confirmCloseDirty: s.general?.confirmCloseDirty ?? true });
|
|
46
|
+
export const windowSettingsOf = (s: ExplorerViewSettings) => ({ sidebarSide: s.window?.sidebarSide ?? 'left' });
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MAX_ROWS = 1000;
|
|
49
|
+
|
|
50
|
+
/** The stored Workbench state with every field the Explorer reads filled with its default. */
|
|
51
|
+
export const withWorkbenchDefaults = (raw?: Partial<ExplorerWorkbench>) => ({
|
|
52
|
+
...raw,
|
|
53
|
+
explorerProjects: raw?.explorerProjects ?? [],
|
|
54
|
+
maxRows: raw?.maxRows ?? DEFAULT_MAX_ROWS,
|
|
55
|
+
results: { ...DEFAULT_RESULT_FORMAT, ...raw?.results },
|
|
56
|
+
copyFormat: raw?.copyFormat ?? ('tsv' satisfies CopyFormat),
|
|
57
|
+
confirmDestructive: raw?.confirmDestructive ?? true,
|
|
58
|
+
queryTimeout: raw?.queryTimeout ?? 0,
|
|
59
|
+
format: { ...DEFAULT_FORMAT_OPTIONS, ...raw?.format },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const sqlRunnerSettings = (s: ReturnType<typeof withWorkbenchDefaults>): SqlRunnerSettings => ({
|
|
63
|
+
maxRows: s.maxRows,
|
|
64
|
+
results: s.results,
|
|
65
|
+
copyFormat: s.copyFormat,
|
|
66
|
+
confirmDestructive: s.confirmDestructive,
|
|
67
|
+
queryTimeout: s.queryTimeout,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const explorer: SectionPath = ['explorer'];
|
|
71
|
+
|
|
72
|
+
/** The `files.*` rows of the Explorer section; the editor's saving rows stay with the app. */
|
|
73
|
+
export const explorerSettingDefs: SettingDef<ExplorerViewSettings>[] = [
|
|
74
|
+
fileBoolSetting('previewTabs', explorer, 10, 'settings.explorer.previewTabs'),
|
|
75
|
+
fileBoolSetting('autoReveal', explorer, 20, 'settings.explorer.autoReveal'),
|
|
76
|
+
fileBoolSetting('breadcrumbs', explorer, 30, 'settings.explorer.breadcrumbs'),
|
|
77
|
+
fileBoolSetting('compactFolders', explorer, 40, 'settings.explorer.compact'),
|
|
78
|
+
fileEnumSetting('sortOrder', explorer, 50, 'settings.explorer.sort', SORT_ORDERS),
|
|
79
|
+
fileBoolSetting('fileNesting', explorer, 60, 'settings.explorer.nesting'),
|
|
80
|
+
{
|
|
81
|
+
id: 'files.nestingPatterns',
|
|
82
|
+
kind: 'custom',
|
|
83
|
+
section: explorer,
|
|
84
|
+
order: 70,
|
|
85
|
+
label: 'settings.explorer.nesting.patterns',
|
|
86
|
+
description: 'settings.explorer.nesting.patterns.hint',
|
|
87
|
+
default: FILE_DEFAULTS.nestingPatterns,
|
|
88
|
+
get: (s) => fileSettingsOf(s.files).nestingPatterns,
|
|
89
|
+
set: (s, v) => ({ files: { ...fileSettingsOf(s.files), nestingPatterns: v as Record<string, string> } }),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'files.exclude',
|
|
93
|
+
kind: 'custom',
|
|
94
|
+
section: explorer,
|
|
95
|
+
order: 80,
|
|
96
|
+
label: 'settings.explorer.exclude',
|
|
97
|
+
description: 'settings.explorer.exclude.hint',
|
|
98
|
+
default: FILE_DEFAULTS.exclude,
|
|
99
|
+
get: (s) => fileSettingsOf(s.files).exclude,
|
|
100
|
+
set: (s, v) => ({ files: { ...fileSettingsOf(s.files), exclude: v as string[] } }),
|
|
101
|
+
},
|
|
102
|
+
fileBoolSetting('confirmDelete', explorer, 90, 'settings.explorer.confirmDelete'),
|
|
103
|
+
fileBoolSetting('confirmDragDrop', explorer, 100, 'settings.explorer.confirmDragDrop'),
|
|
104
|
+
fileBoolSetting('showHidden', explorer, 110, 'settings.explorer.showHidden'),
|
|
105
|
+
fileBoolSetting('restoreTabs', explorer, 120, 'settings.explorer.restoreTabs'),
|
|
106
|
+
fileBoolSetting('markdownToc', explorer, 130, 'settings.explorer.markdownToc'),
|
|
107
|
+
];
|
package/src/view.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { lazy } from 'react';
|
|
2
|
+
import { defineView } from '@deckbuilderkit/ui';
|
|
3
|
+
import { bindExplorerClient, type ExplorerClients } from './client';
|
|
4
|
+
import { explorerViewCommands } from './commands';
|
|
5
|
+
import { en, ptBR } from './i18n';
|
|
6
|
+
import { setMultiDelete } from './options';
|
|
7
|
+
import { explorerSettingDefs, type ExplorerViewSettings } from './settings';
|
|
8
|
+
import { ExcludeSettingField, ExplorerCard, NestingSettingField } from './settings/cards';
|
|
9
|
+
|
|
10
|
+
const ExplorerScreen = lazy(() => import('./ExplorerScreen').then((m) => ({ default: m.ExplorerScreen })));
|
|
11
|
+
|
|
12
|
+
export interface ExplorerViewOptions {
|
|
13
|
+
/** i18n key of the nav section title. */
|
|
14
|
+
group: string;
|
|
15
|
+
order: number;
|
|
16
|
+
/** The server calls the screen makes. */
|
|
17
|
+
client: ExplorerClients;
|
|
18
|
+
/** Whether the tree may delete several entries at once; off when omitted. */
|
|
19
|
+
multiDelete?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function explorerView({ group, order, client, multiDelete = false }: ExplorerViewOptions) {
|
|
23
|
+
bindExplorerClient(client);
|
|
24
|
+
setMultiDelete(multiDelete);
|
|
25
|
+
return defineView<'explorer', ExplorerViewSettings, 'nav.explorer'>({
|
|
26
|
+
id: 'explorer',
|
|
27
|
+
nav: { group, icon: 'folder', label: 'nav.explorer', order },
|
|
28
|
+
render: ({ settings, onSave }) => <ExplorerScreen settings={settings} onSave={onSave} />,
|
|
29
|
+
commands: explorerViewCommands,
|
|
30
|
+
settings: { defs: explorerSettingDefs },
|
|
31
|
+
settingsCards: {
|
|
32
|
+
sections: [{ path: ['explorer'], order: 50, label: 'settings.section.explorer' }],
|
|
33
|
+
cards: { explorer: ExplorerCard },
|
|
34
|
+
fields: { 'files.nestingPatterns': NestingSettingField, 'files.exclude': ExcludeSettingField },
|
|
35
|
+
},
|
|
36
|
+
messages: { en, 'pt-BR': ptBR },
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { type EditorInfo, type ExplorerClient, type ExplorerClients, type FsEntry, type FsRead, type Project, type SearchHit, type TextEncoding } from './client';
|
|
2
|
+
export { type ExplorerMessageKey } from './i18n';
|
|
3
|
+
export { type ExplorerSettings, type ExplorerViewSettings } from './settings';
|
|
4
|
+
export { explorerView, type ExplorerViewOptions } from './view';
|