@itrocks/list 0.7.1 → 0.13.3
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 +99 -1
- package/build.js +31 -13
- package/cjs/access.d.ts +10 -0
- package/cjs/access.js +12 -0
- package/cjs/access.js.map +1 -0
- package/cjs/columns.d.ts +19 -10
- package/cjs/columns.js +60 -23
- package/cjs/columns.js.map +1 -1
- package/cjs/feed.js +1 -1
- package/cjs/feed.js.map +1 -1
- package/cjs/filter.js +24 -5
- package/cjs/filter.js.map +1 -1
- package/cjs/list.d.ts +4 -1
- package/cjs/list.html +9 -9
- package/cjs/list.js +54 -7
- package/cjs/list.js.map +1 -1
- package/cjs/multiple-output.d.ts +4 -0
- package/cjs/multiple-output.js +16 -0
- package/cjs/multiple-output.js.map +1 -0
- package/cjs/projection.d.ts +28 -0
- package/cjs/projection.js +91 -0
- package/cjs/projection.js.map +1 -0
- package/cjs/selector-state.d.ts +22 -0
- package/cjs/selector-state.js +109 -0
- package/cjs/selector-state.js.map +1 -0
- package/cjs/sort.js +8 -3
- package/cjs/sort.js.map +1 -1
- package/column-add.svg +1 -0
- package/column-move.svg +1 -0
- package/column-remove.svg +1 -0
- package/config.yaml +1 -0
- package/list.css +110 -2
- package/package.json +24 -2
- package/property-select.d.ts +54 -0
- package/property-select.js +245 -0
- package/selector-state.d.ts +14 -0
- package/selector-state.js +38 -0
package/list.css
CHANGED
|
@@ -3,10 +3,13 @@ html.column-reorder * {
|
|
|
3
3
|
cursor: grabbing !important;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
main > article[data-action=list] > header {
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
6
9
|
main > article[data-action=list] > header > .summary {
|
|
7
10
|
padding: 13px 10px 12px;
|
|
8
11
|
position: absolute;
|
|
9
|
-
right:
|
|
12
|
+
right: 48px;
|
|
10
13
|
white-space: nowrap;
|
|
11
14
|
}
|
|
12
15
|
main > article[data-action=list] > form {
|
|
@@ -48,6 +51,15 @@ table.objects > * > tr > .column-reorder {
|
|
|
48
51
|
--column-reorder-tint: inset 0 0 0 100vmax rgb(0 0 0 / 2%);
|
|
49
52
|
box-shadow: var(--column-reorder-tint);
|
|
50
53
|
}
|
|
54
|
+
table.objects > * > tr > .column-reorder.column-add-provisional {
|
|
55
|
+
--column-reorder-tint: inset 0 0 0 100vmax rgb(47 137 67 / 5%);
|
|
56
|
+
}
|
|
57
|
+
table.objects > * > tr > .column-reorder.moving {
|
|
58
|
+
--column-reorder-tint: inset 0 0 0 100vmax rgb(38 116 198 / 4%);
|
|
59
|
+
}
|
|
60
|
+
table.objects > * > tr > .column-reorder.removing {
|
|
61
|
+
--column-reorder-tint: inset 0 0 0 100vmax rgb(220 35 45 / 4%);
|
|
62
|
+
}
|
|
51
63
|
table.objects > * > tr > .column-reorder input {
|
|
52
64
|
box-shadow: var(--column-reorder-tint);
|
|
53
65
|
}
|
|
@@ -72,9 +84,40 @@ table.objects > tbody:first-child > tr:nth-child(odd) > td {
|
|
|
72
84
|
table.objects > thead > tr.properties > th {
|
|
73
85
|
user-select: none;
|
|
74
86
|
}
|
|
87
|
+
table.objects > thead > tr.properties > th.moving, table.objects > thead > tr.properties > th.removing {
|
|
88
|
+
z-index: 2;
|
|
89
|
+
}
|
|
90
|
+
table.objects > thead > tr.properties > th.moving::before, table.objects > thead > tr.properties > th.removing::before, table.objects > thead > tr.properties > th.column-add-provisional.column-reorder::before {
|
|
91
|
+
content: "";
|
|
92
|
+
display: inline-block;
|
|
93
|
+
height: 1em;
|
|
94
|
+
margin-left: -1em;
|
|
95
|
+
mask-position: center;
|
|
96
|
+
mask-repeat: no-repeat;
|
|
97
|
+
mask-size: contain;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
transform: translateX(-0.15em);
|
|
100
|
+
vertical-align: -0.22em;
|
|
101
|
+
width: 1em;
|
|
102
|
+
}
|
|
103
|
+
table.objects > thead > tr.properties > th.moving::before {
|
|
104
|
+
background: #2674c6;
|
|
105
|
+
mask-image: url("column-move.svg");
|
|
106
|
+
}
|
|
107
|
+
table.objects > thead > tr.properties > th.removing::before {
|
|
108
|
+
background: #d51d2c;
|
|
109
|
+
mask-image: url("column-remove.svg");
|
|
110
|
+
}
|
|
111
|
+
table.objects > thead > tr.properties > th.column-add-provisional.column-reorder::before {
|
|
112
|
+
background: #2f8943;
|
|
113
|
+
mask-image: url("column-add.svg");
|
|
114
|
+
}
|
|
75
115
|
table.objects > thead > tr.properties > th[data-property] {
|
|
76
116
|
cursor: pointer;
|
|
77
117
|
}
|
|
118
|
+
table.objects > thead > tr.properties > th[data-sortable=false] {
|
|
119
|
+
cursor: default;
|
|
120
|
+
}
|
|
78
121
|
table.objects > thead > tr.properties > th[data-sort] {
|
|
79
122
|
padding-right: calc(0.6em + 16px);
|
|
80
123
|
}
|
|
@@ -84,7 +127,7 @@ table.objects > thead > tr.properties > th[data-sort]::after {
|
|
|
84
127
|
mask: url("sort.svg") right center/24px 24px no-repeat;
|
|
85
128
|
position: absolute;
|
|
86
129
|
right: 0;
|
|
87
|
-
top:
|
|
130
|
+
top: 2px;
|
|
88
131
|
width: 24px;
|
|
89
132
|
}
|
|
90
133
|
table.objects > thead > tr.properties > th[data-sort][data-reverse]::after {
|
|
@@ -134,3 +177,68 @@ table.objects > thead + tbody > tr:nth-child(odd) > td {
|
|
|
134
177
|
:not(article, article > form) > table.objects td:first-child, :not(article, article > form) > table.objects th:first-child {
|
|
135
178
|
border-left: 1px solid #d8d8d8;
|
|
136
179
|
}
|
|
180
|
+
|
|
181
|
+
main > article[data-action=list] > header > .add-column {
|
|
182
|
+
background: white;
|
|
183
|
+
border: 0;
|
|
184
|
+
border-bottom: 1px solid #ccc;
|
|
185
|
+
border-left: 1px solid #ccc;
|
|
186
|
+
border-radius: 0;
|
|
187
|
+
bottom: 0;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
font-size: 16px;
|
|
190
|
+
position: absolute;
|
|
191
|
+
right: 0;
|
|
192
|
+
top: 0;
|
|
193
|
+
width: 44px;
|
|
194
|
+
}
|
|
195
|
+
main > article[data-action=list] > header > .add-column:hover {
|
|
196
|
+
background-color: #f4f4f4;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
table.objects > thead > tr.properties > th {
|
|
200
|
+
line-height: 28px;
|
|
201
|
+
padding-block: 0;
|
|
202
|
+
position: sticky;
|
|
203
|
+
text-align: center;
|
|
204
|
+
}
|
|
205
|
+
table.objects > thead > tr.properties > th.select {
|
|
206
|
+
padding-inline: 0;
|
|
207
|
+
}
|
|
208
|
+
table.objects > thead > tr.properties > th.select::before {
|
|
209
|
+
content: "";
|
|
210
|
+
display: block;
|
|
211
|
+
height: 28px;
|
|
212
|
+
}
|
|
213
|
+
table.objects > thead > tr.properties > th.column-add-provisional::before {
|
|
214
|
+
content: none;
|
|
215
|
+
}
|
|
216
|
+
table.objects > thead > tr.properties > th > input:not([type=checkbox]) {
|
|
217
|
+
border: 0;
|
|
218
|
+
border-top: 1px solid #d8d8d8;
|
|
219
|
+
border-radius: 0;
|
|
220
|
+
display: block;
|
|
221
|
+
height: 28px;
|
|
222
|
+
padding: 0 6px;
|
|
223
|
+
width: 100%;
|
|
224
|
+
}
|
|
225
|
+
table.objects > thead > tr.properties > th > input[name=reset] {
|
|
226
|
+
background: url("filter-reset.svg") center no-repeat;
|
|
227
|
+
color: transparent;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
font-size: 0;
|
|
230
|
+
padding: 0;
|
|
231
|
+
width: 28px;
|
|
232
|
+
}
|
|
233
|
+
table.objects > thead > tr.properties > th > input[data-property-filter] {
|
|
234
|
+
border-left: 1px solid #d8d8d8;
|
|
235
|
+
margin-left: -0.6em;
|
|
236
|
+
width: calc(100% + 1.2em);
|
|
237
|
+
}
|
|
238
|
+
table.objects > thead > tr.properties > th[data-sort] > input[data-property-filter] {
|
|
239
|
+
width: calc(100% + 1.2em + 16px);
|
|
240
|
+
}
|
|
241
|
+
table.objects > thead > tr.properties > th > input:focus {
|
|
242
|
+
box-shadow: inset 0 0 0 2px Highlight, var(--column-reorder-tint, 0 0 transparent);
|
|
243
|
+
outline: none;
|
|
244
|
+
}
|
package/package.json
CHANGED
|
@@ -4,17 +4,32 @@
|
|
|
4
4
|
"email": "baptiste@pillot.fr"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"@itrocks/action": "latest",
|
|
7
8
|
"@itrocks/action-pack": "latest",
|
|
9
|
+
"@itrocks/action-request": "latest",
|
|
8
10
|
"@itrocks/build": "latest",
|
|
9
11
|
"@itrocks/class-type": "latest",
|
|
12
|
+
"@itrocks/class-view": "latest",
|
|
13
|
+
"@itrocks/composition": "latest",
|
|
14
|
+
"@itrocks/core-transformers": "latest",
|
|
15
|
+
"@itrocks/decorator": "latest",
|
|
10
16
|
"@itrocks/list-properties": "latest",
|
|
17
|
+
"@itrocks/property-select": "latest",
|
|
18
|
+
"@itrocks/property-view": "latest",
|
|
11
19
|
"@itrocks/reflect": "latest",
|
|
20
|
+
"@itrocks/reflect-path": "latest",
|
|
12
21
|
"@itrocks/request-response": "latest",
|
|
22
|
+
"@itrocks/route": "latest",
|
|
23
|
+
"@itrocks/sort": "latest",
|
|
24
|
+
"@itrocks/storage": "latest",
|
|
13
25
|
"@itrocks/table": "latest",
|
|
26
|
+
"@itrocks/transformer": "latest",
|
|
14
27
|
"@itrocks/xtarget": "latest"
|
|
15
28
|
},
|
|
16
29
|
"description": "Generic action-based object list navigation in HTML and JSON",
|
|
17
30
|
"devDependencies": {
|
|
31
|
+
"@itrocks/class-file": "latest",
|
|
32
|
+
"@itrocks/fastify-file-session-store": "latest",
|
|
18
33
|
"@types/node": "^24.10",
|
|
19
34
|
"sass": "^1.95",
|
|
20
35
|
"typescript": "^7.0"
|
|
@@ -22,7 +37,14 @@
|
|
|
22
37
|
"engines": {
|
|
23
38
|
"node": ">=24"
|
|
24
39
|
},
|
|
25
|
-
"exports":
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./cjs/list.js",
|
|
42
|
+
"./multiple-output": "./cjs/multiple-output.js",
|
|
43
|
+
"./projection": "./cjs/projection.js",
|
|
44
|
+
"./property-select.js": "./property-select.js",
|
|
45
|
+
"./selector-state": "./cjs/selector-state.js",
|
|
46
|
+
"./selector-state.js": "./selector-state.js"
|
|
47
|
+
},
|
|
26
48
|
"homepage": "https://it.rocks",
|
|
27
49
|
"files": [
|
|
28
50
|
"config.yaml",
|
|
@@ -70,5 +92,5 @@
|
|
|
70
92
|
"build:front": "tsc -p src/front/tsconfig.json",
|
|
71
93
|
"test": "npm run build:cjs && node --test \"test/**/*.test.mjs\""
|
|
72
94
|
},
|
|
73
|
-
"version": "0.
|
|
95
|
+
"version": "0.13.3"
|
|
74
96
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PropertySelectView } from '@itrocks/property-select/front/property-select.js';
|
|
2
|
+
import type { ConsumerPort } from '@itrocks/property-select/property-select.js';
|
|
3
|
+
import type { MetadataProvider } from '@itrocks/property-select/property-select.js';
|
|
4
|
+
import type { PointerSample } from '@itrocks/property-select/property-select.js';
|
|
5
|
+
import type { Outcome } from '@itrocks/property-select/property-select.js';
|
|
6
|
+
import type { SelectionIntent } from '@itrocks/property-select/property-select.js';
|
|
7
|
+
import { TableColumnAdd } from '@itrocks/table/column-add.js';
|
|
8
|
+
export type PaneHandle = {
|
|
9
|
+
close(): void;
|
|
10
|
+
open(): void;
|
|
11
|
+
remove(): void;
|
|
12
|
+
subscribe(listener: (state: {
|
|
13
|
+
present: boolean;
|
|
14
|
+
open: boolean;
|
|
15
|
+
suspended: boolean;
|
|
16
|
+
}) => void): () => void;
|
|
17
|
+
suspend(options?: {
|
|
18
|
+
mobileOnly?: boolean;
|
|
19
|
+
}): {
|
|
20
|
+
restore(): void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type ListSelectorOptions = {
|
|
24
|
+
advice?: readonly string[];
|
|
25
|
+
addition: TableColumnAdd;
|
|
26
|
+
metadata: MetadataProvider;
|
|
27
|
+
mountSelector(content: HTMLElement, title: string): PaneHandle;
|
|
28
|
+
saveColumns(): Promise<void>;
|
|
29
|
+
templateUrl?: string;
|
|
30
|
+
};
|
|
31
|
+
export declare class ListPropertySelect implements ConsumerPort {
|
|
32
|
+
private readonly article;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private readonly abort;
|
|
35
|
+
private readonly button;
|
|
36
|
+
private disposed;
|
|
37
|
+
private drag?;
|
|
38
|
+
private readonly observer;
|
|
39
|
+
private readonly pane;
|
|
40
|
+
private readonly pending;
|
|
41
|
+
readonly view: PropertySelectView;
|
|
42
|
+
constructor(article: HTMLElement, options: ListSelectorOptions);
|
|
43
|
+
private loadColumn;
|
|
44
|
+
private add;
|
|
45
|
+
begin(intent: SelectionIntent, sample: PointerSample): void;
|
|
46
|
+
cancel(id: string): void;
|
|
47
|
+
private previewError;
|
|
48
|
+
dispose(): void;
|
|
49
|
+
end(id: string, sample: PointerSample): Promise<Outcome>;
|
|
50
|
+
mount(): Promise<boolean>;
|
|
51
|
+
move(id: string, sample: PointerSample): void;
|
|
52
|
+
paths(): string[];
|
|
53
|
+
select(intent: SelectionIntent, signal: AbortSignal): Promise<Outcome>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { PropertySelectView } from '@itrocks/property-select/front/property-select.js';
|
|
2
|
+
import { normalisePath } from '@itrocks/property-select/property-select.js';
|
|
3
|
+
import { HttpStateStore } from './selector-state.js';
|
|
4
|
+
export class ListPropertySelect {
|
|
5
|
+
article;
|
|
6
|
+
options;
|
|
7
|
+
abort = new AbortController();
|
|
8
|
+
button;
|
|
9
|
+
disposed = false;
|
|
10
|
+
drag;
|
|
11
|
+
observer;
|
|
12
|
+
pane;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
view;
|
|
15
|
+
constructor(article, options) {
|
|
16
|
+
this.article = article;
|
|
17
|
+
this.options = options;
|
|
18
|
+
const content = document.createElement('div');
|
|
19
|
+
this.button = article.querySelector('.add-column');
|
|
20
|
+
this.pane = options.mountSelector(content, 'Add columns');
|
|
21
|
+
this.view = new PropertySelectView(content, {
|
|
22
|
+
consumer: this, metadata: options.metadata, root: article.dataset.propertyRoot,
|
|
23
|
+
advice: options.advice, stateKey: article.dataset.selectorKey, stateMode: 'session',
|
|
24
|
+
stateStore: new HttpStateStore(), usedPaths: this.paths()
|
|
25
|
+
}, { templateUrl: options.templateUrl });
|
|
26
|
+
this.observer = new MutationObserver(records => {
|
|
27
|
+
if (this.drag && records.some(record => record.target.tagName === 'TBODY'))
|
|
28
|
+
this.view.selector.cancel(this.drag.intent.id, 'row view changed');
|
|
29
|
+
this.view.selector.setConfiguration({ usedPaths: this.paths() });
|
|
30
|
+
});
|
|
31
|
+
this.observer.observe(article.querySelector('thead'), { childList: true, subtree: true });
|
|
32
|
+
this.observer.observe(article.querySelector('tbody'), { childList: true });
|
|
33
|
+
this.button.addEventListener('click', () => this.pane.open(), { signal: this.abort.signal });
|
|
34
|
+
}
|
|
35
|
+
async loadColumn(path, signal) {
|
|
36
|
+
const form = this.article.querySelector('form');
|
|
37
|
+
const table = this.article.querySelector('table.objects');
|
|
38
|
+
const rows = [...table.tBodies[0].rows];
|
|
39
|
+
const ids = rows.map(row => row.querySelector('input[name=id]').value);
|
|
40
|
+
const query = new URLSearchParams({ column: path, rowIds: ids.join(',') });
|
|
41
|
+
const response = await fetch(`${form.action}?${query}`, {
|
|
42
|
+
signal: AbortSignal.any([signal, this.abort.signal])
|
|
43
|
+
});
|
|
44
|
+
const result = await response.json();
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
throw new Error(result.error ?? 'Column unavailable');
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
if (this.disposed || !this.article.isConnected || rows.some((row, index) => table.tBodies[0].rows[index] !== row)
|
|
49
|
+
|| rows.length !== table.tBodies[0].rows.length)
|
|
50
|
+
throw new Error('The row view changed; try again');
|
|
51
|
+
if (result.path !== path)
|
|
52
|
+
throw new Error('Mismatched column response');
|
|
53
|
+
const values = new Map(result.rows.map(row => [String(row.id), row.html]));
|
|
54
|
+
if (ids.some(id => !values.has(id)))
|
|
55
|
+
throw new Error('The row scope changed; reload the list');
|
|
56
|
+
const header = document.createElement('th');
|
|
57
|
+
header.dataset.property = path;
|
|
58
|
+
header.dataset.sortable = String(result.sortable);
|
|
59
|
+
const input = document.createElement('input');
|
|
60
|
+
input.name = `search.${encodeURIComponent(path).replaceAll('.', '%2E')}`;
|
|
61
|
+
input.dataset.propertyFilter = path;
|
|
62
|
+
input.setAttribute('aria-label', `Filter ${result.label}`);
|
|
63
|
+
header.append(result.label, input);
|
|
64
|
+
const cells = ids.map(id => {
|
|
65
|
+
const cell = document.createElement('td');
|
|
66
|
+
cell.innerHTML = values.get(id);
|
|
67
|
+
return cell;
|
|
68
|
+
});
|
|
69
|
+
return { cells, header };
|
|
70
|
+
}
|
|
71
|
+
async add(path, signal) {
|
|
72
|
+
const content = await this.loadColumn(path, signal);
|
|
73
|
+
const outcome = await this.options.addition.add({ create: () => content, id: path });
|
|
74
|
+
await this.options.saveColumns();
|
|
75
|
+
this.view.selector.setConfiguration({ usedPaths: this.paths() });
|
|
76
|
+
return outcome;
|
|
77
|
+
}
|
|
78
|
+
begin(intent, sample) {
|
|
79
|
+
if (this.disposed || this.drag || this.pending.size)
|
|
80
|
+
throw new Error('A column operation is pending');
|
|
81
|
+
const path = normalisePath(intent.path);
|
|
82
|
+
const paths = this.paths();
|
|
83
|
+
const suspension = this.pane.suspend({ mobileOnly: true });
|
|
84
|
+
try {
|
|
85
|
+
const operation = this.options.addition.beginExternal({
|
|
86
|
+
id: path,
|
|
87
|
+
accept: async (signal) => {
|
|
88
|
+
const form = this.article.querySelector('form');
|
|
89
|
+
const query = new URLSearchParams({ column: path, validate: '1' });
|
|
90
|
+
const response = await fetch(`${form.action}?${query}`, {
|
|
91
|
+
signal: AbortSignal.any([signal, this.abort.signal])
|
|
92
|
+
});
|
|
93
|
+
const result = await response.json();
|
|
94
|
+
if (!response.ok || result.path !== path)
|
|
95
|
+
throw new Error(result.error ?? 'Column unavailable');
|
|
96
|
+
},
|
|
97
|
+
create: () => {
|
|
98
|
+
const header = document.createElement('th');
|
|
99
|
+
header.dataset.property = path;
|
|
100
|
+
header.dataset.sortable = 'false';
|
|
101
|
+
header.textContent = path;
|
|
102
|
+
const cells = [...this.article.querySelectorAll('table.objects tbody tr')].map(() => {
|
|
103
|
+
const cell = document.createElement('td');
|
|
104
|
+
cell.textContent = 'Loading…';
|
|
105
|
+
return cell;
|
|
106
|
+
});
|
|
107
|
+
return { cells, header };
|
|
108
|
+
},
|
|
109
|
+
load: async (signal) => {
|
|
110
|
+
try {
|
|
111
|
+
return await this.loadColumn(path, signal);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (!this.disposed && !signal.aborted)
|
|
115
|
+
this.previewError(path);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, sample);
|
|
120
|
+
this.drag = { intent, operation, paths, restore: () => suspension.restore() };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
suspension.restore();
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
cancel(id) {
|
|
128
|
+
if (this.drag?.intent.id !== id)
|
|
129
|
+
return;
|
|
130
|
+
const drag = this.drag;
|
|
131
|
+
drag.operation.cancel();
|
|
132
|
+
this.drag = undefined;
|
|
133
|
+
drag.restore();
|
|
134
|
+
this.view.selector.setConfiguration({ usedPaths: this.paths() });
|
|
135
|
+
}
|
|
136
|
+
previewError(path) {
|
|
137
|
+
const header = [...this.article.querySelectorAll('thead [data-property]')]
|
|
138
|
+
.find(header => header.dataset.property === path);
|
|
139
|
+
if (!header)
|
|
140
|
+
return;
|
|
141
|
+
for (const row of this.article.querySelectorAll('tbody tr'))
|
|
142
|
+
row.cells[header.cellIndex].textContent = 'Unable to load values';
|
|
143
|
+
if (header.querySelector('.column-retry'))
|
|
144
|
+
return;
|
|
145
|
+
const retry = document.createElement('button');
|
|
146
|
+
retry.type = 'button';
|
|
147
|
+
retry.className = 'column-retry';
|
|
148
|
+
retry.textContent = 'Retry values';
|
|
149
|
+
retry.addEventListener('click', event => {
|
|
150
|
+
event.stopPropagation();
|
|
151
|
+
void this.options.addition.retry(path);
|
|
152
|
+
}, { signal: this.abort.signal });
|
|
153
|
+
header.append(retry);
|
|
154
|
+
}
|
|
155
|
+
dispose() {
|
|
156
|
+
if (this.disposed)
|
|
157
|
+
return;
|
|
158
|
+
if (this.drag)
|
|
159
|
+
this.cancel(this.drag.intent.id);
|
|
160
|
+
this.disposed = true;
|
|
161
|
+
this.abort.abort();
|
|
162
|
+
this.observer.disconnect();
|
|
163
|
+
this.view.dispose();
|
|
164
|
+
this.pane.remove();
|
|
165
|
+
this.options.addition.dispose();
|
|
166
|
+
this.button.hidden = true;
|
|
167
|
+
this.pending.clear();
|
|
168
|
+
}
|
|
169
|
+
async end(id, sample) {
|
|
170
|
+
const drag = this.drag;
|
|
171
|
+
if (!drag || drag.intent.id !== id)
|
|
172
|
+
throw new Error('Unknown column transport');
|
|
173
|
+
try {
|
|
174
|
+
const status = await drag.operation.finish(sample);
|
|
175
|
+
if (status !== 'cancelled')
|
|
176
|
+
await this.options.saveColumns();
|
|
177
|
+
if (this.disposed)
|
|
178
|
+
return { ...drag.intent, status: 'cancelled' };
|
|
179
|
+
if (this.drag === drag)
|
|
180
|
+
this.drag = undefined;
|
|
181
|
+
this.view.selector.setConfiguration({ usedPaths: this.paths() });
|
|
182
|
+
return { ...drag.intent, status };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
drag.operation.cancel();
|
|
186
|
+
return { ...drag.intent, status: 'rejected', message: String(error) };
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
if (this.drag === drag)
|
|
190
|
+
this.drag = undefined;
|
|
191
|
+
drag.restore();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async mount() {
|
|
195
|
+
const mounted = await this.view.mount();
|
|
196
|
+
if (mounted && !this.disposed)
|
|
197
|
+
this.button.hidden = false;
|
|
198
|
+
return mounted;
|
|
199
|
+
}
|
|
200
|
+
move(id, sample) {
|
|
201
|
+
if (this.drag?.intent.id === id)
|
|
202
|
+
this.drag.operation.move(sample);
|
|
203
|
+
}
|
|
204
|
+
paths() {
|
|
205
|
+
if (this.drag)
|
|
206
|
+
return [...this.drag.paths];
|
|
207
|
+
return [...this.article.querySelectorAll('thead [data-property]:not(.column-add-provisional)')]
|
|
208
|
+
.map(header => header.dataset.property);
|
|
209
|
+
}
|
|
210
|
+
async select(intent, signal) {
|
|
211
|
+
const path = normalisePath(intent.path);
|
|
212
|
+
try {
|
|
213
|
+
if (this.drag)
|
|
214
|
+
throw new Error('A column transport is pending');
|
|
215
|
+
let status;
|
|
216
|
+
const pending = this.pending.get(path);
|
|
217
|
+
if (pending) {
|
|
218
|
+
await pending;
|
|
219
|
+
status = 'existing';
|
|
220
|
+
}
|
|
221
|
+
else if (this.paths().includes(path))
|
|
222
|
+
status = 'existing';
|
|
223
|
+
else {
|
|
224
|
+
const operation = this.add(path, signal);
|
|
225
|
+
this.pending.set(path, operation);
|
|
226
|
+
try {
|
|
227
|
+
status = await operation;
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (this.pending.get(path) === operation)
|
|
231
|
+
this.pending.delete(path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
signal.throwIfAborted();
|
|
235
|
+
if (this.disposed)
|
|
236
|
+
throw new Error('List disposed');
|
|
237
|
+
this.options.addition.reveal(path);
|
|
238
|
+
this.options.addition.highlight(path);
|
|
239
|
+
return { ...intent, status };
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return { ...intent, message: error instanceof Error ? error.message : 'Column unavailable', status: 'rejected' };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SelectorState } from '@itrocks/property-select/property-select.js';
|
|
2
|
+
import type { StateRecord } from '@itrocks/property-select/property-select.js';
|
|
3
|
+
import type { StateStore } from '@itrocks/property-select/property-select.js';
|
|
4
|
+
/** The key is supplied by the host router; this transport does not derive it from location. */
|
|
5
|
+
export declare class HttpStateStore implements StateStore {
|
|
6
|
+
private readonly endpoint;
|
|
7
|
+
private readonly fetcher;
|
|
8
|
+
private generation?;
|
|
9
|
+
constructor(endpoint?: string, fetcher?: typeof fetch);
|
|
10
|
+
read(key: string, signal?: AbortSignal): Promise<StateRecord>;
|
|
11
|
+
remove(key: string, revision: number): Promise<StateRecord>;
|
|
12
|
+
write(key: string, state: SelectorState, revision: number): Promise<StateRecord>;
|
|
13
|
+
private request;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StateConflictError } from '@itrocks/property-select/property-select.js';
|
|
2
|
+
/** The key is supplied by the host router; this transport does not derive it from location. */
|
|
3
|
+
export class HttpStateStore {
|
|
4
|
+
endpoint;
|
|
5
|
+
fetcher;
|
|
6
|
+
generation;
|
|
7
|
+
constructor(endpoint = '/property-select/state/json', fetcher = globalThis.fetch.bind(globalThis)) {
|
|
8
|
+
this.endpoint = endpoint;
|
|
9
|
+
this.fetcher = fetcher;
|
|
10
|
+
}
|
|
11
|
+
read(key, signal) {
|
|
12
|
+
return this.request('GET', key, undefined, undefined, signal);
|
|
13
|
+
}
|
|
14
|
+
remove(key, revision) {
|
|
15
|
+
return this.request('DELETE', key, undefined, revision);
|
|
16
|
+
}
|
|
17
|
+
write(key, state, revision) {
|
|
18
|
+
return this.request('PUT', key, state, revision);
|
|
19
|
+
}
|
|
20
|
+
async request(method, key, state, revision, signal) {
|
|
21
|
+
const response = await this.fetcher(`${this.endpoint}?${new URLSearchParams({ key })}`, {
|
|
22
|
+
body: method === 'GET' ? undefined : JSON.stringify({ revision, state }), cache: 'no-store',
|
|
23
|
+
credentials: 'same-origin', method, signal,
|
|
24
|
+
headers: { 'Content-Type': 'application/json', 'X-Property-Select-Session': this.generation ?? '' }
|
|
25
|
+
});
|
|
26
|
+
if (response.status === 410)
|
|
27
|
+
throw new Error('The selector session has ended. Reload the page.');
|
|
28
|
+
if (response.status === 409)
|
|
29
|
+
throw new StateConflictError();
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw new Error(`Unable to save selector state (${response.status}). Try again.`);
|
|
32
|
+
const generation = response.headers.get('X-Property-Select-Session') ?? undefined;
|
|
33
|
+
if (this.generation && generation !== this.generation)
|
|
34
|
+
throw new Error('The selector session has ended. Reload the page.');
|
|
35
|
+
this.generation = generation;
|
|
36
|
+
return response.json();
|
|
37
|
+
}
|
|
38
|
+
}
|