@zoobzio/browser 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dist/index.d.mts +331 -0
- package/.dist/index.mjs +236 -0
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/package.json +38 -3
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { Change as Change$1, Changers as Changers$1, Changes as Changes$1, Listener as Listener$1, Middleware as Middleware$1, Options as Options$1 } from "@zoobzio/kit";
|
|
2
|
+
/**
|
|
3
|
+
* Column data type — drives cell rendering and sort behavior.
|
|
4
|
+
*/
|
|
5
|
+
export type ColumnType = "text" | "number" | "date" | "datetime" | "boolean" | "enum" | "currency" | "filesize" | "url" | "image" | "action";
|
|
6
|
+
/**
|
|
7
|
+
* A file column — what the browser renders per file row.
|
|
8
|
+
*/
|
|
9
|
+
export interface BrowserColumn<T> {
|
|
10
|
+
key: keyof T;
|
|
11
|
+
label: string;
|
|
12
|
+
type?: ColumnType;
|
|
13
|
+
align?: "left" | "center" | "right";
|
|
14
|
+
sortable?: boolean;
|
|
15
|
+
sortKey?: string;
|
|
16
|
+
enumValues?: string[];
|
|
17
|
+
}
|
|
18
|
+
export type SortDirection = "asc" | "desc";
|
|
19
|
+
/**
|
|
20
|
+
* The serializable half of a file/folder/bulk action: how it renders. The
|
|
21
|
+
* behavior half is a handler registered under the same key in `Actions`.
|
|
22
|
+
*/
|
|
23
|
+
export interface ActionDescriptor {
|
|
24
|
+
label: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* File action — the live shape the service exposes: descriptor joined with
|
|
28
|
+
* its handler, under the config key. `runAction(key, row)` dispatches by
|
|
29
|
+
* key.
|
|
30
|
+
*/
|
|
31
|
+
export interface FileAction<T> {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
action: (row: T) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Bulk action over the selected files — the live shape the service exposes:
|
|
38
|
+
* descriptor joined with its handler, under the config key.
|
|
39
|
+
* `runBulkAction(key)` dispatches by key over the service's selection.
|
|
40
|
+
*/
|
|
41
|
+
export interface BulkAction {
|
|
42
|
+
key: string;
|
|
43
|
+
label: string;
|
|
44
|
+
action: (selected: Set<string>) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A folder row — fixed on purpose: consumers map their folder data into it,
|
|
48
|
+
* and folder rendering customizes through the view. Files stay generic;
|
|
49
|
+
* folders do not.
|
|
50
|
+
*/
|
|
51
|
+
export type BrowserFolder = {
|
|
52
|
+
key: string;
|
|
53
|
+
label: string;
|
|
54
|
+
count?: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* One step of the navigation trail. State keeps the full crumb (key and
|
|
58
|
+
* label) so breadcrumb labels survive navigation without a refetch; fetch
|
|
59
|
+
* params and events carry keys only.
|
|
60
|
+
*/
|
|
61
|
+
export type BrowserCrumb = {
|
|
62
|
+
key: string;
|
|
63
|
+
label: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Parameters passed to the fetch action. The browser owns navigation and
|
|
67
|
+
* file sort: `path` is the folder-key trail from the root (`[]` = root).
|
|
68
|
+
* Sorting applies to files only — folders always render alphabetically,
|
|
69
|
+
* enforced by the service regardless of fetch order.
|
|
70
|
+
*/
|
|
71
|
+
export interface BrowserFetchParams {
|
|
72
|
+
path: string[];
|
|
73
|
+
sortField: string | null;
|
|
74
|
+
sortDirection: SortDirection;
|
|
75
|
+
}
|
|
76
|
+
export interface BrowserFetchResult<T> {
|
|
77
|
+
folders: BrowserFolder[];
|
|
78
|
+
files: T[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Folder action — rendered per-folder in an actions menu. The live shape
|
|
82
|
+
* the service exposes: descriptor joined with its handler, under the config
|
|
83
|
+
* key. `runFolderAction(key, folder)` dispatches by key.
|
|
84
|
+
*/
|
|
85
|
+
export type FolderAction = {
|
|
86
|
+
key: string;
|
|
87
|
+
label: string;
|
|
88
|
+
action: (folder: BrowserFolder) => void;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Config the consumer provides — pure serializable data. Actions are
|
|
92
|
+
* descriptor records: the key is the action's identity, shared with the
|
|
93
|
+
* handler record in `Actions`. `actions`/`bulkActions` cover files
|
|
94
|
+
* (selection is files-only); `folderActions` covers folder rows.
|
|
95
|
+
*/
|
|
96
|
+
export type Config<T> = {
|
|
97
|
+
columns: BrowserColumn<T>[];
|
|
98
|
+
fileKey: keyof T;
|
|
99
|
+
/**
|
|
100
|
+
* The label of the root crumb. Defaults to `Home`.
|
|
101
|
+
*/
|
|
102
|
+
rootLabel?: string;
|
|
103
|
+
actions?: Record<string, ActionDescriptor>;
|
|
104
|
+
folderActions?: Record<string, ActionDescriptor>;
|
|
105
|
+
bulkActions?: Record<string, ActionDescriptor>;
|
|
106
|
+
/**
|
|
107
|
+
* The BCP 47 locale cells format in. Defaults to `en-US`.
|
|
108
|
+
*/
|
|
109
|
+
locale?: string;
|
|
110
|
+
/**
|
|
111
|
+
* The ISO 4217 code currency cells format in. Defaults to `USD`.
|
|
112
|
+
*/
|
|
113
|
+
currency?: string;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The state container the service reads and writes: the listing, the load
|
|
117
|
+
* flags, the trail, the file sort, and the selection. The container can be
|
|
118
|
+
* a plain object or a reactive proxy. The service replaces each member as a
|
|
119
|
+
* whole, so a proxy tracks every change.
|
|
120
|
+
*/
|
|
121
|
+
export type State<T> = {
|
|
122
|
+
folders: BrowserFolder[];
|
|
123
|
+
files: T[];
|
|
124
|
+
loading: boolean;
|
|
125
|
+
initialized: boolean;
|
|
126
|
+
path: BrowserCrumb[];
|
|
127
|
+
sortField: string | null;
|
|
128
|
+
sortDirection: SortDirection;
|
|
129
|
+
selected: Set<string>;
|
|
130
|
+
};
|
|
131
|
+
export type StateKey = keyof State<unknown>;
|
|
132
|
+
/**
|
|
133
|
+
* The kit's state mechanisms bound to this service's container: per-field
|
|
134
|
+
* middleware, the read/write options, and the change objects.
|
|
135
|
+
*/
|
|
136
|
+
export type Middleware<T> = Middleware$1<State<T>>;
|
|
137
|
+
export type Options<T> = Options$1<State<T>>;
|
|
138
|
+
export type Changes<T> = Changes$1<State<T>>;
|
|
139
|
+
export type Change<T, K extends StateKey = StateKey> = Change$1<State<T>, K>;
|
|
140
|
+
export type Changers<T> = Changers$1<State<T>>;
|
|
141
|
+
/**
|
|
142
|
+
* The consumer-supplied behavior: the fetch mechanism plus handlers keyed
|
|
143
|
+
* like the config's descriptor records.
|
|
144
|
+
*/
|
|
145
|
+
export type Actions<T> = {
|
|
146
|
+
fetch: (params: BrowserFetchParams, service: Service<T>) => Promise<BrowserFetchResult<T>>;
|
|
147
|
+
actions?: Record<string, (row: T) => void>;
|
|
148
|
+
folderActions?: Record<string, (folder: BrowserFolder) => void>;
|
|
149
|
+
bulkActions?: Record<string, (selected: Set<string>) => void>;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* The events of a browser service, keyed by name. Each value is the
|
|
153
|
+
* argument list of a listener. The service emits after the write lands, so
|
|
154
|
+
* a listener reads the new state.
|
|
155
|
+
*
|
|
156
|
+
* - `change`: any field of the container was written.
|
|
157
|
+
* - `updated`: a fetch settled, success or failure. `path` is the key trail,
|
|
158
|
+
* `folders`/`files` the section lengths.
|
|
159
|
+
* - `navigated`: the trail changed through `open` or `navigate`.
|
|
160
|
+
*/
|
|
161
|
+
export type Events<T> = {
|
|
162
|
+
change: [change: Change<T>];
|
|
163
|
+
updated: [event: {
|
|
164
|
+
id: string;
|
|
165
|
+
path: string[];
|
|
166
|
+
folders: number;
|
|
167
|
+
files: number;
|
|
168
|
+
}];
|
|
169
|
+
navigated: [event: {
|
|
170
|
+
id: string;
|
|
171
|
+
path: string[];
|
|
172
|
+
}];
|
|
173
|
+
};
|
|
174
|
+
export type Event<T> = keyof Events<T>;
|
|
175
|
+
export type Listener<T, E extends Event<T>> = Listener$1<Events<T>, E>;
|
|
176
|
+
/**
|
|
177
|
+
* The imperative contract: unwrapped state + derived + the full method
|
|
178
|
+
* surface. `BrowserService<T>` declares `implements` against this, and the
|
|
179
|
+
* fetch action receives it.
|
|
180
|
+
*/
|
|
181
|
+
export type Service<T> = {
|
|
182
|
+
readonly id: string;
|
|
183
|
+
readonly config: Config<T>;
|
|
184
|
+
readonly columns: BrowserColumn<T>[];
|
|
185
|
+
readonly fileKey: keyof T;
|
|
186
|
+
readonly actions: FileAction<T>[];
|
|
187
|
+
readonly folderActions: FolderAction[];
|
|
188
|
+
readonly bulkActions: BulkAction[];
|
|
189
|
+
readonly rootLabel: string;
|
|
190
|
+
readonly locale: string;
|
|
191
|
+
readonly currency: string;
|
|
192
|
+
readonly folders: BrowserFolder[];
|
|
193
|
+
readonly files: T[];
|
|
194
|
+
readonly loading: boolean;
|
|
195
|
+
readonly initialized: boolean;
|
|
196
|
+
readonly path: BrowserCrumb[];
|
|
197
|
+
readonly sortField: string | null;
|
|
198
|
+
readonly sortDirection: SortDirection;
|
|
199
|
+
readonly selected: Set<string>;
|
|
200
|
+
readonly pathKeys: string[];
|
|
201
|
+
readonly depth: number;
|
|
202
|
+
/**
|
|
203
|
+
* The breadcrumb trail: the root crumb, then one crumb per trail step.
|
|
204
|
+
* Each crumb's key is its joined path prefix, unique even when one folder
|
|
205
|
+
* key recurs at several depths; its index is what `navigate` takes.
|
|
206
|
+
*/
|
|
207
|
+
readonly trail: BrowserCrumb[];
|
|
208
|
+
readonly isAllSelected: boolean;
|
|
209
|
+
readonly isIndeterminate: boolean;
|
|
210
|
+
readonly selectAllState: boolean | "indeterminate";
|
|
211
|
+
readonly colSpan: number;
|
|
212
|
+
readonly hasSelection: boolean;
|
|
213
|
+
readonly isSelectable: boolean;
|
|
214
|
+
readonly hasActions: boolean;
|
|
215
|
+
readonly hasFolderActions: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Whether the current folder has settled with neither folders nor files.
|
|
218
|
+
* Gated on `initialized` and `loading`, so a view shows the listing, not
|
|
219
|
+
* the empty state, until the first fetch resolves.
|
|
220
|
+
*/
|
|
221
|
+
readonly isEmpty: boolean;
|
|
222
|
+
open(folder: BrowserFolder): void;
|
|
223
|
+
navigate(index: number): void;
|
|
224
|
+
sortBy(field: string): void;
|
|
225
|
+
sortFieldFor(col: BrowserColumn<T>): string;
|
|
226
|
+
isSorted(col: BrowserColumn<T>): boolean;
|
|
227
|
+
keyOf(row: T): string;
|
|
228
|
+
toggleRow(key: string): void;
|
|
229
|
+
toggleAll(): void;
|
|
230
|
+
clearSelection(): void;
|
|
231
|
+
isRowSelected(row: T): boolean;
|
|
232
|
+
runAction(key: string, row: T): void;
|
|
233
|
+
runFolderAction(key: string, folder: BrowserFolder): void;
|
|
234
|
+
runBulkAction(key: string): void;
|
|
235
|
+
init(): Promise<boolean>;
|
|
236
|
+
fetch(): Promise<void>;
|
|
237
|
+
/**
|
|
238
|
+
* Subscribes a listener to an event. Listeners run in subscription order.
|
|
239
|
+
* An error in a listener propagates to the caller of the write. Returns a
|
|
240
|
+
* function that removes the listener.
|
|
241
|
+
*/
|
|
242
|
+
on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
|
|
243
|
+
};
|
|
244
|
+
/** Default file sort direction. */
|
|
245
|
+
export declare const BROWSER_DEFAULT_SORT_DIRECTION: SortDirection;
|
|
246
|
+
/** The key of the root crumb: the trail's first entry, above every folder. */
|
|
247
|
+
export declare const BROWSER_ROOT_KEY = "";
|
|
248
|
+
/** The label of the root crumb when the config names none. */
|
|
249
|
+
export declare const BROWSER_DEFAULT_ROOT_LABEL = "Home";
|
|
250
|
+
/** The locale cells format in when the config names none. */
|
|
251
|
+
export declare const BROWSER_DEFAULT_LOCALE = "en-US";
|
|
252
|
+
/** The currency code currency cells format in when the config names none. */
|
|
253
|
+
export declare const BROWSER_DEFAULT_CURRENCY = "USD";
|
|
254
|
+
/**
|
|
255
|
+
* The fields of the container, for key-driven iteration independent of a
|
|
256
|
+
* container's runtime keys.
|
|
257
|
+
*/
|
|
258
|
+
export declare const BROWSER_STATE_FIELDS: Record<StateKey, null>;
|
|
259
|
+
/**
|
|
260
|
+
* Builds the initial state container. The result is a plain object. Wrap it
|
|
261
|
+
* in a reactive proxy to track reads and writes. The trail starts at the
|
|
262
|
+
* root and sorting starts unset, so no config is needed.
|
|
263
|
+
*/
|
|
264
|
+
export declare const createBrowserState: <T>() => State<T>;
|
|
265
|
+
/**
|
|
266
|
+
* The headless browser machine. Owns the folder trail, the file sort, and
|
|
267
|
+
* the file selection; delegates data loading to `wiring.fetch` and joins
|
|
268
|
+
* the config's action descriptors to the handlers in `wiring`.
|
|
269
|
+
*
|
|
270
|
+
* The service reads and writes the `state` container through the `options`
|
|
271
|
+
* middleware. The container can be a plain object or a reactive proxy; the
|
|
272
|
+
* service replaces each member as a whole. Each write emits `change`, and
|
|
273
|
+
* the domain methods emit `updated` and `navigated`. `on` subscribes.
|
|
274
|
+
*/
|
|
275
|
+
export declare class BrowserService<T> implements Service<T> {
|
|
276
|
+
readonly id: string;
|
|
277
|
+
readonly config: Config<T>;
|
|
278
|
+
private readonly wiring;
|
|
279
|
+
readonly columns: BrowserColumn<T>[];
|
|
280
|
+
readonly fileKey: keyof T;
|
|
281
|
+
readonly actions: Service<T>["actions"];
|
|
282
|
+
readonly folderActions: Service<T>["folderActions"];
|
|
283
|
+
readonly bulkActions: Service<T>["bulkActions"];
|
|
284
|
+
private readonly state;
|
|
285
|
+
private readonly emitter;
|
|
286
|
+
constructor(id: string, config: Config<T>, state: State<T>, wiring: Actions<T>, options?: Options<T>);
|
|
287
|
+
on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
|
|
288
|
+
get folders(): BrowserFolder[];
|
|
289
|
+
get files(): T[];
|
|
290
|
+
get loading(): boolean;
|
|
291
|
+
get initialized(): boolean;
|
|
292
|
+
get path(): BrowserCrumb[];
|
|
293
|
+
get sortField(): string | null;
|
|
294
|
+
get sortDirection(): SortDirection;
|
|
295
|
+
get selected(): Set<string>;
|
|
296
|
+
get rootLabel(): string;
|
|
297
|
+
get locale(): string;
|
|
298
|
+
get currency(): string;
|
|
299
|
+
get pathKeys(): string[];
|
|
300
|
+
get depth(): number;
|
|
301
|
+
get trail(): BrowserCrumb[];
|
|
302
|
+
get isAllSelected(): boolean;
|
|
303
|
+
get isIndeterminate(): boolean;
|
|
304
|
+
get selectAllState(): boolean | "indeterminate";
|
|
305
|
+
get colSpan(): number;
|
|
306
|
+
get hasSelection(): boolean;
|
|
307
|
+
get isSelectable(): boolean;
|
|
308
|
+
get hasActions(): boolean;
|
|
309
|
+
get hasFolderActions(): boolean;
|
|
310
|
+
get isEmpty(): boolean;
|
|
311
|
+
keyOf(row: T): string;
|
|
312
|
+
/** Descends into a folder: push its crumb, drop the selection, refetch. */
|
|
313
|
+
open(folder: BrowserFolder): void;
|
|
314
|
+
/**
|
|
315
|
+
* Jumps to a trail position: `0` is the root, `depth` is where the
|
|
316
|
+
* browser already stands (a no-op, like clicking the current crumb).
|
|
317
|
+
*/
|
|
318
|
+
navigate(index: number): void;
|
|
319
|
+
sortBy(field: string): void;
|
|
320
|
+
sortFieldFor(col: BrowserColumn<T>): string;
|
|
321
|
+
isSorted(col: BrowserColumn<T>): boolean;
|
|
322
|
+
toggleRow(key: string): void;
|
|
323
|
+
toggleAll(): void;
|
|
324
|
+
clearSelection(): void;
|
|
325
|
+
isRowSelected(row: T): boolean;
|
|
326
|
+
runAction(key: string, row: T): void;
|
|
327
|
+
runFolderAction(key: string, folder: BrowserFolder): void;
|
|
328
|
+
runBulkAction(key: string): void;
|
|
329
|
+
init(): Promise<boolean>;
|
|
330
|
+
fetch(): Promise<void>;
|
|
331
|
+
}
|
package/.dist/index.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { entries } from "objectively";
|
|
2
|
+
import { makeEmitter, makeProxy } from "@zoobzio/kit";
|
|
3
|
+
const BROWSER_DEFAULT_SORT_DIRECTION = "asc";
|
|
4
|
+
const BROWSER_ROOT_KEY = "";
|
|
5
|
+
const BROWSER_DEFAULT_ROOT_LABEL = "Home";
|
|
6
|
+
const BROWSER_DEFAULT_LOCALE = "en-US";
|
|
7
|
+
const BROWSER_DEFAULT_CURRENCY = "USD";
|
|
8
|
+
const BROWSER_STATE_FIELDS = {
|
|
9
|
+
folders: null,
|
|
10
|
+
files: null,
|
|
11
|
+
loading: null,
|
|
12
|
+
initialized: null,
|
|
13
|
+
path: null,
|
|
14
|
+
sortField: null,
|
|
15
|
+
sortDirection: null,
|
|
16
|
+
selected: null
|
|
17
|
+
};
|
|
18
|
+
const createBrowserState = () => ({
|
|
19
|
+
folders: [],
|
|
20
|
+
files: [],
|
|
21
|
+
loading: false,
|
|
22
|
+
initialized: false,
|
|
23
|
+
path: [],
|
|
24
|
+
sortField: null,
|
|
25
|
+
sortDirection: "asc",
|
|
26
|
+
selected: /* @__PURE__ */ new Set()
|
|
27
|
+
});
|
|
28
|
+
const sortFolders = (folders) => [...folders].sort((a, b) => a.label.localeCompare(b.label));
|
|
29
|
+
var BrowserService = class {
|
|
30
|
+
id;
|
|
31
|
+
config;
|
|
32
|
+
wiring;
|
|
33
|
+
columns;
|
|
34
|
+
fileKey;
|
|
35
|
+
actions;
|
|
36
|
+
folderActions;
|
|
37
|
+
bulkActions;
|
|
38
|
+
state;
|
|
39
|
+
emitter = makeEmitter();
|
|
40
|
+
constructor(id, config, state, wiring, options = {}) {
|
|
41
|
+
this.id = id;
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.wiring = wiring;
|
|
44
|
+
this.state = makeProxy(state, BROWSER_STATE_FIELDS, options, this.emitter);
|
|
45
|
+
this.columns = config.columns;
|
|
46
|
+
this.fileKey = config.fileKey;
|
|
47
|
+
this.actions = entries(config.actions ?? {}).map(([key, descriptor]) => ({
|
|
48
|
+
...descriptor,
|
|
49
|
+
key,
|
|
50
|
+
action: (row) => this.runAction(key, row)
|
|
51
|
+
}));
|
|
52
|
+
this.folderActions = entries(config.folderActions ?? {}).map(([key, descriptor]) => ({
|
|
53
|
+
...descriptor,
|
|
54
|
+
key,
|
|
55
|
+
action: (folder) => this.runFolderAction(key, folder)
|
|
56
|
+
}));
|
|
57
|
+
this.bulkActions = entries(config.bulkActions ?? {}).map(([key, descriptor]) => ({
|
|
58
|
+
...descriptor,
|
|
59
|
+
key,
|
|
60
|
+
action: (selected) => wiring.bulkActions?.[key]?.(selected)
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
on(event, listener) {
|
|
64
|
+
return this.emitter.on(event, listener);
|
|
65
|
+
}
|
|
66
|
+
get folders() {
|
|
67
|
+
return this.state.folders;
|
|
68
|
+
}
|
|
69
|
+
get files() {
|
|
70
|
+
return this.state.files;
|
|
71
|
+
}
|
|
72
|
+
get loading() {
|
|
73
|
+
return this.state.loading;
|
|
74
|
+
}
|
|
75
|
+
get initialized() {
|
|
76
|
+
return this.state.initialized;
|
|
77
|
+
}
|
|
78
|
+
get path() {
|
|
79
|
+
return this.state.path;
|
|
80
|
+
}
|
|
81
|
+
get sortField() {
|
|
82
|
+
return this.state.sortField;
|
|
83
|
+
}
|
|
84
|
+
get sortDirection() {
|
|
85
|
+
return this.state.sortDirection;
|
|
86
|
+
}
|
|
87
|
+
get selected() {
|
|
88
|
+
return this.state.selected;
|
|
89
|
+
}
|
|
90
|
+
get rootLabel() {
|
|
91
|
+
return this.config.rootLabel ?? "Home";
|
|
92
|
+
}
|
|
93
|
+
get locale() {
|
|
94
|
+
return this.config.locale ?? "en-US";
|
|
95
|
+
}
|
|
96
|
+
get currency() {
|
|
97
|
+
return this.config.currency ?? "USD";
|
|
98
|
+
}
|
|
99
|
+
get pathKeys() {
|
|
100
|
+
return this.path.map((crumb) => crumb.key);
|
|
101
|
+
}
|
|
102
|
+
get depth() {
|
|
103
|
+
return this.path.length;
|
|
104
|
+
}
|
|
105
|
+
get trail() {
|
|
106
|
+
const keys = this.pathKeys;
|
|
107
|
+
return [{
|
|
108
|
+
key: "",
|
|
109
|
+
label: this.rootLabel
|
|
110
|
+
}, ...this.path.map((crumb, index) => ({
|
|
111
|
+
key: keys.slice(0, index + 1).join("/"),
|
|
112
|
+
label: crumb.label
|
|
113
|
+
}))];
|
|
114
|
+
}
|
|
115
|
+
get isAllSelected() {
|
|
116
|
+
if (!this.files.length) return false;
|
|
117
|
+
return this.files.every((row) => this.selected.has(this.keyOf(row)));
|
|
118
|
+
}
|
|
119
|
+
get isIndeterminate() {
|
|
120
|
+
if (!this.selected.size) return false;
|
|
121
|
+
return !this.isAllSelected;
|
|
122
|
+
}
|
|
123
|
+
get selectAllState() {
|
|
124
|
+
if (this.isIndeterminate) return "indeterminate";
|
|
125
|
+
return this.isAllSelected;
|
|
126
|
+
}
|
|
127
|
+
get colSpan() {
|
|
128
|
+
return this.columns.length + (this.hasActions || this.hasFolderActions ? 1 : 0) + (this.isSelectable ? 1 : 0);
|
|
129
|
+
}
|
|
130
|
+
get hasSelection() {
|
|
131
|
+
return this.selected.size > 0;
|
|
132
|
+
}
|
|
133
|
+
get isSelectable() {
|
|
134
|
+
return this.bulkActions.length > 0;
|
|
135
|
+
}
|
|
136
|
+
get hasActions() {
|
|
137
|
+
return this.actions.length > 0;
|
|
138
|
+
}
|
|
139
|
+
get hasFolderActions() {
|
|
140
|
+
return this.folderActions.length > 0;
|
|
141
|
+
}
|
|
142
|
+
get isEmpty() {
|
|
143
|
+
return this.initialized && !this.loading && !this.folders.length && !this.files.length;
|
|
144
|
+
}
|
|
145
|
+
keyOf(row) {
|
|
146
|
+
return String(row[this.config.fileKey]);
|
|
147
|
+
}
|
|
148
|
+
open(folder) {
|
|
149
|
+
this.state.path = [...this.path, {
|
|
150
|
+
key: folder.key,
|
|
151
|
+
label: folder.label
|
|
152
|
+
}];
|
|
153
|
+
this.state.selected = /* @__PURE__ */ new Set();
|
|
154
|
+
this.fetch();
|
|
155
|
+
this.emitter.emit("navigated", {
|
|
156
|
+
id: this.id,
|
|
157
|
+
path: this.pathKeys
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
navigate(index) {
|
|
161
|
+
if (index < 0 || index >= this.depth) return;
|
|
162
|
+
this.state.path = this.path.slice(0, index);
|
|
163
|
+
this.state.selected = /* @__PURE__ */ new Set();
|
|
164
|
+
this.fetch();
|
|
165
|
+
this.emitter.emit("navigated", {
|
|
166
|
+
id: this.id,
|
|
167
|
+
path: this.pathKeys
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
sortBy(field) {
|
|
171
|
+
if (this.sortField === field) this.state.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
|
|
172
|
+
else {
|
|
173
|
+
this.state.sortField = field;
|
|
174
|
+
this.state.sortDirection = "asc";
|
|
175
|
+
}
|
|
176
|
+
this.fetch();
|
|
177
|
+
}
|
|
178
|
+
sortFieldFor(col) {
|
|
179
|
+
return col.sortKey ?? String(col.key);
|
|
180
|
+
}
|
|
181
|
+
isSorted(col) {
|
|
182
|
+
return this.sortField === this.sortFieldFor(col);
|
|
183
|
+
}
|
|
184
|
+
toggleRow(key) {
|
|
185
|
+
const next = new Set(this.selected);
|
|
186
|
+
if (next.has(key)) next.delete(key);
|
|
187
|
+
else next.add(key);
|
|
188
|
+
this.state.selected = next;
|
|
189
|
+
}
|
|
190
|
+
toggleAll() {
|
|
191
|
+
if (this.isAllSelected) this.state.selected = /* @__PURE__ */ new Set();
|
|
192
|
+
else this.state.selected = new Set(this.files.map((row) => this.keyOf(row)));
|
|
193
|
+
}
|
|
194
|
+
clearSelection() {
|
|
195
|
+
this.state.selected = /* @__PURE__ */ new Set();
|
|
196
|
+
}
|
|
197
|
+
isRowSelected(row) {
|
|
198
|
+
return this.selected.has(this.keyOf(row));
|
|
199
|
+
}
|
|
200
|
+
runAction(key, row) {
|
|
201
|
+
this.wiring.actions?.[key]?.(row);
|
|
202
|
+
}
|
|
203
|
+
runFolderAction(key, folder) {
|
|
204
|
+
this.wiring.folderActions?.[key]?.(folder);
|
|
205
|
+
}
|
|
206
|
+
runBulkAction(key) {
|
|
207
|
+
this.wiring.bulkActions?.[key]?.(this.selected);
|
|
208
|
+
}
|
|
209
|
+
async init() {
|
|
210
|
+
if (this.initialized) return true;
|
|
211
|
+
this.state.initialized = true;
|
|
212
|
+
await this.fetch();
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async fetch() {
|
|
216
|
+
this.state.loading = true;
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.wiring.fetch({
|
|
219
|
+
path: this.pathKeys,
|
|
220
|
+
sortField: this.sortField,
|
|
221
|
+
sortDirection: this.sortDirection
|
|
222
|
+
}, this);
|
|
223
|
+
this.state.folders = sortFolders(result.folders);
|
|
224
|
+
this.state.files = result.files;
|
|
225
|
+
} finally {
|
|
226
|
+
this.state.loading = false;
|
|
227
|
+
this.emitter.emit("updated", {
|
|
228
|
+
id: this.id,
|
|
229
|
+
path: this.pathKeys,
|
|
230
|
+
folders: this.folders.length,
|
|
231
|
+
files: this.files.length
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
export { BROWSER_DEFAULT_CURRENCY, BROWSER_DEFAULT_LOCALE, BROWSER_DEFAULT_ROOT_LABEL, BROWSER_DEFAULT_SORT_DIRECTION, BROWSER_ROOT_KEY, BROWSER_STATE_FIELDS, BrowserService, createBrowserState };
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alexander Thorwaldson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zoobzio/browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless data browser service. Pure TypeScript, no framework dependency.
|
|
4
|
+
|
|
5
|
+
Owns: the folder trail (open a folder, jump to a crumb), the file sort,
|
|
6
|
+
the file selection, and the join of action descriptors to their handlers.
|
|
7
|
+
Delegates: data loading (`fetch`) and state storage (the container).
|
|
8
|
+
Folders always come back alphabetical; file order is the fetch's.
|
|
9
|
+
|
|
10
|
+
The package defines its own column and action shapes (`BrowserColumn`,
|
|
11
|
+
`ActionDescriptor`, `FileAction`, `FolderAction`, `BulkAction`,
|
|
12
|
+
`SortDirection`); it depends on no other package.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { BrowserService, createBrowserState } from "@zoobzio/browser";
|
|
18
|
+
|
|
19
|
+
const config = {
|
|
20
|
+
columns: [{ key: "name", label: "Name", sortable: true }],
|
|
21
|
+
fileKey: "id",
|
|
22
|
+
actions: { download: { label: "Download" } },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const browser = new BrowserService("assets", config, createBrowserState(), {
|
|
26
|
+
fetch: async (params) => api.assets(params), // { folders, files }
|
|
27
|
+
actions: { download: (file) => save(file) },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await browser.init();
|
|
31
|
+
browser.open({ key: "media", label: "Media" }); // descend, refetch
|
|
32
|
+
browser.navigate(0); // back to the root
|
|
33
|
+
browser.sortBy("name"); // asc, then desc on repeat
|
|
34
|
+
browser.toggleRow("1");
|
|
35
|
+
browser.runAction("download", file); // dispatch a file action by key
|
|
36
|
+
browser.runBulkAction("purge"); // a bulk action over the selection
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## The trail
|
|
40
|
+
|
|
41
|
+
`trail` is the breadcrumb from the root: the root crumb (`rootLabel`,
|
|
42
|
+
default `Home`) followed by one crumb per step of `path`. Each crumb's key
|
|
43
|
+
is its joined path prefix, so keys stay unique when one folder key recurs
|
|
44
|
+
at several depths, and a crumb's index in `trail` is what `navigate` takes.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
browser.trail; // [{ key: "", label: "Home" }, { key: "media", label: "Media" }]
|
|
48
|
+
browser.navigate(browser.trail.findIndex((c) => c.key === "media"));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## The state container
|
|
52
|
+
|
|
53
|
+
The service reads and writes the `state` container: the listing, the load
|
|
54
|
+
flags, the trail, the file sort, and the selection. The container can be a
|
|
55
|
+
plain object or a reactive proxy. The service replaces each member as a
|
|
56
|
+
whole, so a proxy tracks every change.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
createBrowserState(); // plain: tests, Node
|
|
60
|
+
reactive(createBrowserState()); // Vue
|
|
61
|
+
useState(`browser-${id}`, () => createBrowserState()).value; // Nuxt, shared by id
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Middleware
|
|
65
|
+
|
|
66
|
+
`options.get.state.<field>` and `options.set.state.<field>` receive the value
|
|
67
|
+
of the field and return the value the service uses. A missing slot passes the
|
|
68
|
+
value on.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
new BrowserService(id, config, state, wiring, {
|
|
72
|
+
set: { state: { sortField: (field) => field?.toLowerCase() ?? null } },
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Events
|
|
77
|
+
|
|
78
|
+
`on(event, listener)` subscribes. Listeners run in subscription order, after
|
|
79
|
+
the write lands. The return value removes the listener.
|
|
80
|
+
|
|
81
|
+
- `change`: a field of the container was written. `{ field, before, after }`, narrowed by `field`.
|
|
82
|
+
- `updated`: a fetch settled, success or failure. `{ id, path, folders, files }` — the key trail and section lengths.
|
|
83
|
+
- `navigated`: the trail changed through `open` or `navigate`. `{ id, path }`.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const off = browser.on("navigated", ({ path }) => history.push(path));
|
|
87
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoobzio/browser",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Headless data browser service: folder trail navigation, file sort, selection, action joins. No framework dependency.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Alex Thorwaldson",
|
|
8
|
+
"email": "alex@zoobz.io",
|
|
9
|
+
"url": "https://zoobz.io"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/zoobzio/foundation.git",
|
|
14
|
+
"directory": "packages/browser"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
".dist"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./.dist/index.mjs",
|
|
22
|
+
"types": "./.dist/index.d.mts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./.dist/index.d.mts",
|
|
26
|
+
"import": "./.dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"objectively": "^0.3.0",
|
|
32
|
+
"@zoobzio/kit": "0.0.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "obuild",
|
|
36
|
+
"stub": "obuild --stub",
|
|
37
|
+
"lint": "oxlint",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
}
|
|
6
41
|
}
|