@itrocks/list 0.13.6 → 0.16.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 CHANGED
@@ -185,16 +185,33 @@ the dependency hook. The configured data source remains responsible for storage
185
185
  `GET /<business-route>/list?column=<path>&rowIds=<comma-separated-identifiers>` uses the HTML action and returns a JSON
186
186
  column descriptor with rendered cells keyed by root id. It intersects visible ids with the current filters and mandatory
187
187
  scope, makes one projection and performs no count. Invalid selections return 400. Full entity JSON remains available
188
- through `/list/json`. Column configuration saves use the established `columns` parameter and return 204.
188
+ through `/list/json`. Adding `columns=<ordered-paths>` saves the column configuration in the same request.
189
+ Configuration-only requests return 204. The browser serialises both kinds of request to preserve the latest order.
189
190
 
190
191
  The browser entry `@itrocks/list/property-select.js` exports `ListPropertySelect`, with explicit metadata, table
191
192
  addition, column persistence and narrow pane ports. The framework mounts it only on compatible lists and disposes it on
192
- navigation. It reserves pending paths, confirms accepted additions, reveals/highlights duplicates without moving focus,
193
- and checks row identity before insertion. Failed selection leaves the context intact. It loads only currently visible
194
- row ids through the current feed position; changing the row view while loading rejects the obsolete response with a
195
- retry message. Clicks and desktop drags create empty columns and immediately load values asynchronously through the
196
- public table transaction. A new dragged column stays hidden until the pointer first enters the table, so its values can
197
- arrive before it appears. Drag admission waits for drop.
193
+ navigation. It reserves pending paths and confirms accepted additions. The checkbox, its cell and the property name
194
+ toggle terminal and relation columns. The checkbox clears immediately before the red removal flash. Reselecting during that flash cancels removal,
195
+ checks the property again and highlights the retained column green, then saves the restored configuration.
196
+ The removal configuration is sent at the initial click, independently of the animation; deleting the cells sends no
197
+ additional request.
198
+ After a completed removal, a later click returns the column immediately right of its former left neighbour, or left of its former right neighbour if the left one is absent. If neither remains,
199
+ it is appended. Neighbours are kept only in this pane instance's memory and cleared on folding, closure or disposal;
200
+ they never enter session state. Drag placement continues to follow the drop position.
201
+ An explicit addition intention always requests addition: an existing column is revealed and highlighted green without removal.
202
+ Property names and checkbox cells toggle the same selection; chevrons and intermediate path segments navigate.
203
+ The consumer checks row identity before insertion. Failed selection leaves the context intact. It loads only currently
204
+ visible row ids through the current feed position; changing the row view while loading rejects the obsolete response with a
205
+ discreet red background. Clicks commit the translated full-path heading and filter locally, before any server response.
206
+ One asynchronous request
207
+ then saves the column order and loads the values; there is no separate admission request. A new drag calls the same
208
+ addition code as a click with `{ effects: false, hidden: true }`, then moves the committed column through the standard
209
+ table reorder operation with `{ adding: true, removal: true }`. Its combined request loads values immediately while
210
+ cells remain hidden, without width animation, flash or automatic scroll. On first pointer entry, the column appears
211
+ with any values already received and uses a green background and plus indicator. An outside drop deletes it through
212
+ the standard header-origin removal and saves once, even if the pointer never entered. Cancelling a new addition also
213
+ removes it. Dropping inside saves only a changed column order; it never reloads values.
214
+ The optional `request(query, signal)` port shares the list persistence queue.
198
215
  Existing-column moves and removals only save the column configuration. The header contains each label and its filter
199
216
  together, so it uses the table addition plugin's single-header contract.
200
217
 
@@ -227,15 +244,13 @@ The browser displays recoverable persistence errors.
227
244
 
228
245
  ## External column transport
229
246
 
230
- The list consumer supplies immediate empty cells, authorises admission and loads all currently loaded row ids through
231
- the same projection/rendering route used for click additions. `validate=1` on a `column` request checks representation
232
- and permission without selecting records; it prevents unsupported object and nested plural paths from being confirmed
233
- on drop.
234
- Admission is independent of the optional values request. A slow or failed values request leaves valid drops accepted;
235
- failed cells expose `Retry values` without another addition or recent entry.
247
+ The list consumer supplies cells containing a non-breaking space and loads all currently loaded row ids through
248
+ one combined configuration/projection request after the local commit. A failed values request leaves the column
249
+ in place, with a discreet red background on its data cells. The spaces remain; no error text, retry button or automatic
250
+ retry is added. The translated full path comes from the selector's displayed row and available branch metadata.
236
251
 
237
- Pending paths and the active drag are reserved before asynchronous work. Used paths retain their committed order
238
- throughout the drag, and configuration saves exclude provisional cells. New accepted drops feed recents; moves and
252
+ Pending paths and the active drag are reserved before asynchronous work. Used paths include the new column immediately
253
+ and retain their committed order throughout the drag. New accepted drops feed recents; moves and
239
254
  cancellations do not. Row-view changes and view disposal cancel transport. The desktop pane stays open, and capture is
240
255
  owned by the selector's surviving source controller. The consumer imports no home implementation, SQL or hydration code.
241
256
 
@@ -245,6 +260,15 @@ cancellation/rejection; it never focuses the search field. View disposal removes
245
260
  restoration. Custom pane adapters may implement the same optional suspension argument without importing `home`.
246
261
 
247
262
  `node test/browser-requests.mjs` checks request counts and real mouse gestures in Chromium with synthetic HTTP data.
248
- It covers initial mount, click placeholders, hidden drag preloading, reveal on table entry, values for 80 loaded rows,
249
- outside cancellation, duplicates, reordering and removal.
263
+ It covers initial mount, click placeholders, hidden drag additions, values for 80 loaded rows before entry or drop,
264
+ green addition indicators, outside removal before and after entry, terminal toggles and reordering.
265
+ `node test/browser-removal-cancel.mjs` checks immediate unchecking and cancellation during the red flash through
266
+ checkbox, cell and name clicks, for terminal and relation columns, with an immediate removal save followed by a restored configuration save.
267
+ `node test/browser-column-positions.mjs` checks neighbour-based reinsertion in the real table, both fallbacks,
268
+ and memory expiry on closure, including closure during a pending removal. It starts its own synthetic HTTP fixture.
250
269
  Set `PS_PLAYWRIGHT_MODULE` to an installed Playwright module when using a different runtime.
270
+
271
+ `node test/browser-toggle.mjs` exercises the real framework list against the synthetic recipe database on port 13316
272
+ (`PS_DATABASE_PORT` overrides it). It starts and stops its own application instance and checks desktop keyboard, touch,
273
+ reduced motion, horizontal reveal, removal and temporary-row transitions. `PS_TOGGLE_URL` reuses an existing instance
274
+ of that recipe instead. The synthetic database must already be running; this test never resets it.
package/build.js CHANGED
@@ -11,24 +11,27 @@ import { tableByElement } from '@itrocks/table/table.js';
11
11
  import { xTargetCall } from '@itrocks/xtarget/xtarget.js';
12
12
  build('article[data-action="list"] > form > table.objects', table => {
13
13
  let saving = Promise.resolve();
14
- const saveColumns = () => {
14
+ const request = (query, signal) => {
15
+ const form = table.closest('form');
16
+ const result = saving.then(() => fetch(`${form.action}?${query}`, { keepalive: true, signal }));
17
+ saving = result.then(() => { }, () => { });
18
+ return result;
19
+ };
20
+ const saveColumns = (paths) => {
15
21
  const columns = table.tHead?.rows[0].querySelectorAll(':scope > th[data-property]:not(.column-add-provisional)');
16
22
  const form = table.closest('form');
17
23
  if (!columns || !form)
18
24
  return Promise.resolve();
19
- const properties = Array.from(columns, column => encodeURIComponent(column.dataset.property ?? ''));
20
- const url = form.action + '?columns=' + properties.join(',');
21
- saving = saving.catch(() => { }).then(async () => {
22
- const response = await fetch(url, { keepalive: true });
25
+ const properties = paths ?? Array.from(columns, column => column.dataset.property ?? '');
26
+ return request(new URLSearchParams({ columns: properties.join(',') })).then(response => {
23
27
  if (!response.ok)
24
28
  throw new Error('Unable to save columns');
25
29
  });
26
- return saving;
27
30
  };
28
31
  const tableColumnReorder = new TableColumnReorder({
29
32
  isReorderable: header => header.dataset.property !== undefined,
30
- onRemove: saveColumns,
31
- onReorder: saveColumns
33
+ onRemove: () => saveColumns(),
34
+ onReorder: () => saveColumns()
32
35
  });
33
36
  const tableLink = new TableLink({
34
37
  call: url => xTargetCall(url, 'main'),
@@ -46,7 +49,7 @@ build('article[data-action="list"] > form > table.objects', table => {
46
49
  ]
47
50
  });
48
51
  table.dispatchEvent(new CustomEvent('itrocks:list-ready', {
49
- bubbles: true, detail: { addition, instance, saveColumns }
52
+ bubbles: true, detail: { addition, instance, request, saveColumns }
50
53
  }));
51
54
  table.addEventListener('click', event => {
52
55
  const element = event.target;
package/cjs/list.js CHANGED
@@ -89,7 +89,7 @@ let List = (() => {
89
89
  for (const field of column.fields ?? [])
90
90
  await access.authorise(`${column.name}.${field}`);
91
91
  }
92
- if (request.request.data.columns !== undefined) {
92
+ if (request.request.data.columns !== undefined && typeof request.request.data.column !== 'string') {
93
93
  (0, persist_2.save)(this, request.request, type);
94
94
  return this.htmlResponse('', 204);
95
95
  }
package/cjs/list.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"list.js","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,4CAAwD;AAExD,4CAAwD;AACxD,4CAAwD;AAExD,0CAAuD;AACvD,0CAAuD;AACvD,8CAAyD;AAEzD,8CAAyD;AACzD,8CAAyD;AACzD,qCAAiD;AACjD,uCAAkD;AAClD,uCAAkD;AAClD,iCAA+C;AAC/C,qCAAiD;AACjD,uCAAkD;AAClD,uCAAkD;AAClD,6CAAqD;AACrD,qDAAyD;AACzD,qDAAyD;AACzD,iCAA+C;AAE/C,mCAAwC;AAA/B,uGAAA,aAAa,OAAA;IAKT,IAAI;4BAFhB,IAAA,aAAI,EAAC,OAAO,EAAE,KAAK,CAAC,EACpB,IAAA,aAAK,EAAC,OAAO,CAAC;;;;sBACsC,eAAM;oBAAd,SAAQ,WAAS;;;;YAA9D,6KAuGC;;;YAvGY,uDAAI;;QAGhB,OAAO,GAAG,IAAI,iBAAO,CAAA;QACrB,IAAI,GAAM,IAAI,WAAI,CAAA;QAClB,MAAM,GAAI,IAAI,eAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAClC,IAAI,GAAM,IAAI,WAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEhC,KAAK,CAAC,IAAI,CAAC,OAAmB,EAAE,KAAqB;YAEpD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;YAC5B,MAAM,IAAI,GAAM,IAAI,CAAC,IAAI,CAAA;YACzB,MAAM,MAAM,GAAI,IAAI,CAAC,MAAM,CAAA;YAC3B,MAAM,IAAI,GAAM,IAAI,CAAC,IAAI,CAAA;YACzB,MAAM,IAAI,GAAM,OAAO,CAAC,IAAI,CAAA;YAE5B,IAAA,cAAI,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YAEjC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACxC,MAAM,MAAM,GAAG,MAAM,IAAA,mBAAU,EAAC,OAAO,CAAC,CAAA;YACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC9B,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;gBACnC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;oBAAE,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAA;YAC3F,CAAC;YACD,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAChD,IAAA,cAAI,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBACjC,OAAO,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,GAAG,CAAC,CAAA;YAClC,CAAC;YAED,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YAC/B,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YACjC,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;YAC9E,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YAErC,IAAA,cAAI,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;YACxE,MAAM,MAAM,GAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;YAC/D,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;YACpE,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;gBAAE,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;YAC/E,IAAI,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACrD,IAAI,CAAC;oBACJ,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAA;oBACxC,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;oBAC5B,MAAM,MAAM,GAAG,IAAI,uBAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;oBACzC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;wBAAE,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAA;oBACnF,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,KAAK,GAAG;wBACxC,OAAO,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;oBACnF,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAA;oBACvC,IAAI,OAAO,GAAG,KAAK,QAAQ;wBAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;oBACnF,MAAM,MAAM,GAAG,MAAM,IAAA,wBAAc,EAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,IAAI,qBAAW,CAC3E,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CACzB,CAAC,CAAC,CAAA;oBACH,OAAO,IAAI,CAAC,YAAY,CAAC;wBACxB,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ;wBACpD,IAAI,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC,GAAG,EAAC,EAAE,CAAC,CAAC;4BACrD,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;yBACvD,CAAC,CAAC,CAAC;qBACJ,CAAC,CAAA;gBACH,CAAC;gBACD,OAAO,KAAK,EAAE,CAAC;oBACd,MAAM,OAAO,GAAG,KAAK,YAAY,wBAAc,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB;wBAClF,CAAC,CAAC,oEAAoE;wBACtE,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAA;oBAChE,OAAO,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,CAAC,CAAA;gBAClD,CAAC;YACF,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,IAAA,wBAAc,EAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;YAElG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,OAAO,IAAI,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC5E,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,IAAA,oBAAU,GAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YACpD,MAAM,KAAK,GAAG,IAAA,eAAO,EAAC,IAAI,CAAC,CAAA;YAE3B,MAAM,cAAc,GAAoB,EAAE,CAAA;YAC1C,MAAM,gBAAgB,GAAkB,EAAE,CAAA;YAC1C,KAAK,MAAM,MAAM,IAAI,IAAA,mBAAU,EAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChF,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,cAAc,CAAA;gBAC9E,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACrB,CAAC;YAED,IAAI,iBAAqC,CAAA;YACzC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACtB,MAAM,KAAK,GAAG,MAAM,IAAA,uCAAsB,EAAC,OAAO,CAAC,CAAA;gBACnD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;gBAC9F,iBAAiB,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAA;YAClF,CAAC;YACD,OAAO,IAAI,CAAC,oBAAoB,CAC/B,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM;gBAClF,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAA,iCAAgB,EAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EACpF,OAAO,EACP,SAAS,GAAG,YAAY,CACxB,CAAA;QACF,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,OAAmB,EAAE,KAAqB;YAEpD,MAAM,OAAO,GAAG,MAAM,IAAA,oBAAU,GAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YAC9D,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAClC,CAAC;;;;AArGW,oBAAI;AAyGjB,iFAAiF;AACjF,kBAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"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"}
package/list.css CHANGED
@@ -51,7 +51,7 @@ table.objects > * > tr > .column-reorder {
51
51
  --column-reorder-tint: inset 0 0 0 100vmax rgb(0 0 0 / 2%);
52
52
  box-shadow: var(--column-reorder-tint);
53
53
  }
54
- table.objects > * > tr > .column-reorder.column-add-provisional {
54
+ table.objects > * > tr > .column-reorder.adding {
55
55
  --column-reorder-tint: inset 0 0 0 100vmax rgb(47 137 67 / 5%);
56
56
  }
57
57
  table.objects > * > tr > .column-reorder.moving {
@@ -84,10 +84,10 @@ table.objects > tbody:first-child > tr:nth-child(odd) > td {
84
84
  table.objects > thead > tr.properties > th {
85
85
  user-select: none;
86
86
  }
87
- table.objects > thead > tr.properties > th.moving, table.objects > thead > tr.properties > th.removing {
87
+ table.objects > thead > tr.properties > th.adding, table.objects > thead > tr.properties > th.moving, table.objects > thead > tr.properties > th.removing {
88
88
  z-index: 2;
89
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 {
90
+ table.objects > thead > tr.properties > th.adding::before, table.objects > thead > tr.properties > th.moving::before, table.objects > thead > tr.properties > th.removing::before {
91
91
  content: "";
92
92
  display: inline-block;
93
93
  height: 1em;
@@ -108,7 +108,7 @@ table.objects > thead > tr.properties > th.removing::before {
108
108
  background: #d51d2c;
109
109
  mask-image: url("column-remove.svg");
110
110
  }
111
- table.objects > thead > tr.properties > th.column-add-provisional.column-reorder::before {
111
+ table.objects > thead > tr.properties > th.adding::before {
112
112
  background: #2f8943;
113
113
  mask-image: url("column-add.svg");
114
114
  }
package/package.json CHANGED
@@ -93,5 +93,5 @@
93
93
  "build:front": "tsc -p src/front/tsconfig.json",
94
94
  "test": "npm run build:cjs && node --test \"test/**/*.test.mjs\""
95
95
  },
96
- "version": "0.13.6"
96
+ "version": "0.16.3"
97
97
  }
@@ -25,7 +25,8 @@ export type ListSelectorOptions = {
25
25
  addition: TableColumnAdd;
26
26
  metadata: MetadataProvider;
27
27
  mountSelector(content: HTMLElement, title: string): PaneHandle;
28
- saveColumns(): Promise<void>;
28
+ request?(query: URLSearchParams, signal: AbortSignal): Promise<Response>;
29
+ saveColumns(paths?: readonly string[]): Promise<void>;
29
30
  templateUrl?: string;
30
31
  };
31
32
  export declare class ListPropertySelect implements ConsumerPort {
@@ -33,24 +34,28 @@ export declare class ListPropertySelect implements ConsumerPort {
33
34
  private readonly options;
34
35
  private readonly abort;
35
36
  private readonly button;
37
+ private readonly content;
36
38
  private disposed;
37
39
  private drag?;
38
40
  private readonly observer;
39
41
  private readonly pane;
40
42
  private readonly pending;
43
+ private readonly removals;
44
+ private removedColumns;
45
+ private readonly unsubscribePane;
41
46
  readonly view: PropertySelectView;
42
47
  constructor(article: HTMLElement, options: ListSelectorOptions);
43
48
  private loadColumn;
44
49
  private add;
45
50
  begin(intent: SelectionIntent, sample: PointerSample): void;
46
51
  private columnSource;
47
- private validateColumn;
48
52
  cancel(id: string): void;
49
- private previewError;
53
+ private displayedPaths;
50
54
  dispose(): void;
51
55
  end(id: string, sample: PointerSample): Promise<Outcome>;
52
56
  mount(): Promise<boolean>;
53
57
  move(id: string, sample: PointerSample): void;
54
58
  paths(): string[];
59
+ private remove;
55
60
  select(intent: SelectionIntent, signal: AbortSignal): Promise<Outcome>;
56
61
  }
@@ -6,26 +6,35 @@ export class ListPropertySelect {
6
6
  options;
7
7
  abort = new AbortController();
8
8
  button;
9
+ content = document.createElement('div');
9
10
  disposed = false;
10
11
  drag;
11
12
  observer;
12
13
  pane;
13
14
  pending = new Map();
15
+ removals = new Map();
16
+ removedColumns = new Map();
17
+ unsubscribePane;
14
18
  view;
15
19
  constructor(article, options) {
16
20
  this.article = article;
17
21
  this.options = options;
18
- const content = document.createElement('div');
22
+ const content = this.content;
19
23
  this.button = article.querySelector('.add-column');
20
- this.pane = options.mountSelector(content, 'Add columns');
21
24
  const initial = article.dataset.selectorBootstrap
22
25
  ? JSON.parse(decodeURIComponent(article.dataset.selectorBootstrap)) : undefined;
26
+ const translate = (text) => initial?.translations?.[text] ?? text;
27
+ this.pane = options.mountSelector(content, translate('Add columns'));
28
+ this.unsubscribePane = this.pane.subscribe(state => {
29
+ if (!state.open || !state.present)
30
+ this.removedColumns = new Map();
31
+ });
23
32
  this.view = new PropertySelectView(content, {
24
33
  consumer: this, metadata: options.metadata, root: article.dataset.propertyRoot,
25
34
  advice: options.advice, stateKey: article.dataset.selectorKey, stateMode: 'session',
26
35
  stateStore: new HttpStateStore(undefined, undefined, initial && {
27
36
  ...initial, key: article.dataset.selectorKey
28
- }), usedPaths: this.paths()
37
+ }), translate, usedPaths: this.paths()
29
38
  }, { template: initial?.template, templateUrl: options.templateUrl });
30
39
  this.observer = new MutationObserver(records => {
31
40
  if (this.drag && records.some(record => record.target.tagName === 'TBODY'))
@@ -41,10 +50,11 @@ export class ListPropertySelect {
41
50
  const table = this.article.querySelector('table.objects');
42
51
  const rows = [...table.tBodies[0].rows];
43
52
  const ids = rows.map(row => row.querySelector('input[name=id]').value);
44
- const query = new URLSearchParams({ column: path, rowIds: ids.join(',') });
45
- const response = await fetch(`${form.action}?${query}`, {
46
- signal: AbortSignal.any([signal, this.abort.signal])
47
- });
53
+ const query = new URLSearchParams({ column: path, columns: this.paths().join(','), rowIds: ids.join(',') });
54
+ const combined = AbortSignal.any([signal, this.abort.signal]);
55
+ const response = this.options.request
56
+ ? await this.options.request(query, combined)
57
+ : await fetch(`${form.action}?${query}`, { signal: combined });
48
58
  const result = await response.json();
49
59
  if (!response.ok)
50
60
  throw new Error(result.error ?? 'Column unavailable');
@@ -64,6 +74,16 @@ export class ListPropertySelect {
64
74
  input.name = `search.${encodeURIComponent(path).replaceAll('.', '%2E')}`;
65
75
  input.dataset.propertyFilter = path;
66
76
  input.setAttribute('aria-label', `Filter ${result.label}`);
77
+ const previous = [...table.tHead.rows[0].cells].find(header => header.dataset.property === path);
78
+ const previousInput = previous?.querySelector('input[data-property-filter]');
79
+ input.value = previousInput?.value ?? '';
80
+ if (previousInput && document.activeElement === previousInput)
81
+ requestAnimationFrame(() => {
82
+ if (!input.isConnected || document.activeElement !== document.body)
83
+ return;
84
+ input.focus();
85
+ input.setSelectionRange(previousInput.selectionStart, previousInput.selectionEnd);
86
+ });
67
87
  header.append(result.label, input);
68
88
  const cells = ids.map(id => {
69
89
  const cell = document.createElement('td');
@@ -72,16 +92,28 @@ export class ListPropertySelect {
72
92
  });
73
93
  return { cells, header };
74
94
  }
75
- async add(path, signal) {
95
+ add(path, signal, options = {}) {
76
96
  const source = this.columnSource(path);
77
- const operation = this.options.addition.begin(source);
97
+ const operation = this.options.addition.begin(source, options);
78
98
  try {
79
- await this.validateColumn(path, signal);
80
99
  signal.throwIfAborted();
81
100
  if (this.disposed)
82
101
  throw new Error('List disposed');
102
+ const position = this.removedColumns.get(path);
103
+ if (position) {
104
+ const headers = [...this.article.querySelectorAll('thead [data-property]:not(.column-add-provisional)')];
105
+ const left = headers.find(header => header.dataset.property === position.left);
106
+ const right = headers.find(header => header.dataset.property === position.right);
107
+ const anchor = left ?? right;
108
+ if (anchor) {
109
+ let index = anchor.cellIndex + (left ? 1 : 0);
110
+ if (operation.header.cellIndex < index)
111
+ index--;
112
+ operation.moveTo(index);
113
+ }
114
+ }
83
115
  const outcome = operation.commit();
84
- await this.options.saveColumns();
116
+ this.removedColumns.delete(path);
85
117
  this.view.selector.setConfiguration({ usedPaths: this.paths() });
86
118
  return outcome;
87
119
  }
@@ -94,21 +126,14 @@ export class ListPropertySelect {
94
126
  if (this.disposed || this.drag || this.pending.size)
95
127
  throw new Error('A column operation is pending');
96
128
  const path = normalisePath(intent.path);
97
- const paths = this.paths();
129
+ const added = !this.paths().includes(path);
98
130
  const suspension = this.pane.suspend({ mobileOnly: true });
99
131
  try {
100
- let drop;
101
- const dropped = new Promise(resolve => { drop = resolve; });
102
- const source = this.columnSource(path);
103
- // Existing columns already have their metadata and cells. Only new drops need admission.
104
- if (!paths.includes(path))
105
- source.accept = async (signal) => {
106
- await dropped;
107
- signal.throwIfAborted();
108
- await this.validateColumn(path, signal);
109
- };
110
- const operation = this.options.addition.beginExternal(source, sample);
111
- this.drag = { drop, intent, operation, paths, restore: () => suspension.restore() };
132
+ if (added)
133
+ this.add(path, this.abort.signal, { effects: false, hidden: true });
134
+ const paths = this.paths();
135
+ const operation = this.options.addition.beginExternal(this.columnSource(path), sample, { adding: added, removal: true });
136
+ this.drag = { added, intent, operation, paths, restore: () => suspension.restore() };
112
137
  }
113
138
  catch (error) {
114
139
  suspension.restore();
@@ -116,10 +141,17 @@ export class ListPropertySelect {
116
141
  }
117
142
  }
118
143
  columnSource(path) {
119
- const result = this.view.selector.search.results.find(result => result.property.path === path);
120
- const property = result?.property || this.view.selector.context?.properties.find(property => property.path === path);
121
- const label = result ? [...result.segments.map(segment => segment.label), result.property.label].join('.')
122
- : property?.label || path;
144
+ const selector = this.view.selector;
145
+ const row = this.content.querySelector(`.ps-property[data-path="${CSS.escape(path)}"]`);
146
+ const result = selector.search.results.find(result => result.property.path === path);
147
+ const properties = selector.branchContexts.flatMap(context => context.properties);
148
+ const label = row?.title || selector.usedProperties.find(property => property.path === path)?.label
149
+ || path.split('.').map((name, index, names) => {
150
+ const prefix = names.slice(0, index + 1).join('.');
151
+ return properties.find(property => property.path === prefix)?.label
152
+ || result?.segments.find(segment => segment.path === prefix)?.label
153
+ || (prefix === path ? result?.property.label || row?.querySelector('.ps-main')?.textContent : '') || name;
154
+ }).join('.');
123
155
  return {
124
156
  id: path,
125
157
  create: () => {
@@ -127,33 +159,27 @@ export class ListPropertySelect {
127
159
  header.dataset.property = path;
128
160
  header.dataset.sortable = 'false';
129
161
  header.textContent = label;
162
+ const input = document.createElement('input');
163
+ input.name = `search.${encodeURIComponent(path).replaceAll('.', '%2E')}`;
164
+ input.dataset.propertyFilter = path;
165
+ input.setAttribute('aria-label', `Filter ${label}`);
166
+ header.append(input);
130
167
  const cells = [...this.article.querySelectorAll('table.objects tbody tr')]
131
- .map(() => document.createElement('td'));
168
+ .map(() => {
169
+ const cell = document.createElement('td');
170
+ cell.textContent = '\u00a0';
171
+ return cell;
172
+ });
132
173
  return { cells, header };
133
174
  },
134
175
  load: async (signal) => {
176
+ // Commit the placeholder before reading the column configuration.
177
+ await Promise.resolve();
135
178
  signal.throwIfAborted();
136
- try {
137
- return await this.loadColumn(path, signal);
138
- }
139
- catch (error) {
140
- if (!this.disposed && !signal.aborted)
141
- this.previewError(path);
142
- throw error;
143
- }
179
+ return this.loadColumn(path, signal);
144
180
  }
145
181
  };
146
182
  }
147
- async validateColumn(path, signal) {
148
- const form = this.article.querySelector('form');
149
- const query = new URLSearchParams({ column: path, validate: '1' });
150
- const response = await fetch(`${form.action}?${query}`, {
151
- signal: AbortSignal.any([signal, this.abort.signal])
152
- });
153
- const result = await response.json();
154
- if (!response.ok || result.path !== path)
155
- throw new Error(result.error ?? 'Column unavailable');
156
- }
157
183
  cancel(id) {
158
184
  if (this.drag?.intent.id !== id)
159
185
  return;
@@ -163,24 +189,10 @@ export class ListPropertySelect {
163
189
  drag.restore();
164
190
  this.view.selector.setConfiguration({ usedPaths: this.paths() });
165
191
  }
166
- previewError(path) {
167
- const header = [...this.article.querySelectorAll('thead [data-property]')]
168
- .find(header => header.dataset.property === path);
169
- if (!header)
170
- return;
171
- for (const row of this.article.querySelectorAll('tbody tr'))
172
- row.cells[header.cellIndex].textContent = 'Unable to load values';
173
- if (header.querySelector('.column-retry'))
174
- return;
175
- const retry = document.createElement('button');
176
- retry.type = 'button';
177
- retry.className = 'column-retry';
178
- retry.textContent = 'Retry values';
179
- retry.addEventListener('click', event => {
180
- event.stopPropagation();
181
- void this.options.addition.retry(path);
182
- }, { signal: this.abort.signal });
183
- header.append(retry);
192
+ displayedPaths() {
193
+ return [...this.article.querySelectorAll('thead [data-property]:not(.column-add-provisional)')]
194
+ .map(header => header.dataset.property)
195
+ .filter(path => !this.removals.has(path) || this.removals.get(path).signal.aborted);
184
196
  }
185
197
  dispose() {
186
198
  if (this.disposed)
@@ -190,22 +202,30 @@ export class ListPropertySelect {
190
202
  this.disposed = true;
191
203
  this.abort.abort();
192
204
  this.observer.disconnect();
205
+ this.unsubscribePane();
206
+ this.removedColumns.clear();
193
207
  this.view.dispose();
194
208
  this.pane.remove();
195
209
  this.options.addition.dispose();
196
210
  this.button.hidden = true;
197
211
  this.pending.clear();
212
+ for (const removal of this.removals.values())
213
+ removal.abort();
214
+ this.removals.clear();
198
215
  }
199
216
  async end(id, sample) {
200
217
  const drag = this.drag;
201
218
  if (!drag || drag.intent.id !== id)
202
219
  throw new Error('Unknown column transport');
203
220
  try {
204
- const finished = drag.operation.finish(sample);
205
- drag.drop();
206
- const status = await finished;
207
- if (status !== 'cancelled')
208
- await this.options.saveColumns();
221
+ let status = await drag.operation.finish(sample);
222
+ if (status !== 'cancelled') {
223
+ this.removedColumns.delete(drag.intent.path);
224
+ if (status === 'moved' && this.displayedPaths().join(',') !== drag.paths.join(','))
225
+ await this.options.saveColumns();
226
+ if (drag.added && status === 'moved')
227
+ status = 'added';
228
+ }
209
229
  if (this.disposed)
210
230
  return { ...drag.intent, status: 'cancelled' };
211
231
  if (this.drag === drag)
@@ -234,10 +254,42 @@ export class ListPropertySelect {
234
254
  this.drag.operation.move(sample);
235
255
  }
236
256
  paths() {
237
- if (this.drag)
238
- return [...this.drag.paths];
239
- return [...this.article.querySelectorAll('thead [data-property]:not(.column-add-provisional)')]
240
- .map(header => header.dataset.property);
257
+ return this.drag ? [...this.drag.paths] : this.displayedPaths();
258
+ }
259
+ async remove(path, signal) {
260
+ const positions = this.removedColumns;
261
+ const paths = this.paths();
262
+ const index = paths.indexOf(path);
263
+ const position = { left: paths[index - 1], right: paths[index + 1] };
264
+ const removal = new AbortController();
265
+ this.removals.set(path, removal);
266
+ // Publish the unchecked state before starting the table's removal flash.
267
+ this.view.selector.setConfiguration({ usedPaths: this.paths() });
268
+ try {
269
+ const saving = this.options.saveColumns(this.paths());
270
+ const animation = this.options.addition.remove(path, AbortSignal.any([signal, removal.signal]));
271
+ const [removed] = await Promise.all([animation, saving]);
272
+ if (!removed) {
273
+ if (removal.signal.aborted && !signal.aborted && !this.disposed) {
274
+ await this.options.saveColumns(this.paths());
275
+ return 'existing';
276
+ }
277
+ throw new Error('Column removal cancelled');
278
+ }
279
+ if (!this.disposed && positions === this.removedColumns && index >= 0)
280
+ positions.set(path, position);
281
+ return 'removed';
282
+ }
283
+ catch (error) {
284
+ removal.abort();
285
+ throw error;
286
+ }
287
+ finally {
288
+ if (this.removals.get(path) === removal)
289
+ this.removals.delete(path);
290
+ if (!this.disposed)
291
+ this.view.selector.setConfiguration({ usedPaths: this.paths() });
292
+ }
241
293
  }
242
294
  async select(intent, signal) {
243
295
  const path = normalisePath(intent.path);
@@ -247,13 +299,27 @@ export class ListPropertySelect {
247
299
  let status;
248
300
  const pending = this.pending.get(path);
249
301
  if (pending) {
250
- await pending;
302
+ const removal = this.removals.get(path);
303
+ if (removal && !removal.signal.aborted) {
304
+ removal.abort();
305
+ this.view.selector.setConfiguration({ usedPaths: this.paths() });
306
+ }
307
+ else if (removal && intent.action !== 'add') {
308
+ await pending;
309
+ return this.select(intent, signal);
310
+ }
311
+ const outcome = await pending;
312
+ if (removal && outcome === 'removed')
313
+ return this.select({ ...intent, action: 'add' }, signal);
251
314
  status = 'existing';
252
315
  }
253
- else if (this.paths().includes(path))
254
- status = 'existing';
255
316
  else {
256
- const operation = this.add(path, signal);
317
+ const existing = this.paths().includes(path);
318
+ const operation = existing && (intent.action !== 'add')
319
+ ? this.remove(path, signal)
320
+ : existing
321
+ ? Promise.resolve('existing')
322
+ : Promise.resolve(this.add(path, signal));
257
323
  this.pending.set(path, operation);
258
324
  try {
259
325
  status = await operation;
@@ -266,8 +332,11 @@ export class ListPropertySelect {
266
332
  signal.throwIfAborted();
267
333
  if (this.disposed)
268
334
  throw new Error('List disposed');
269
- this.options.addition.reveal(path);
270
- this.options.addition.highlight(path);
335
+ if (status !== 'removed' && this.paths().includes(path)) {
336
+ this.options.addition.reveal(path);
337
+ if (status === 'existing')
338
+ this.options.addition.highlight(path);
339
+ }
271
340
  return { ...intent, status };
272
341
  }
273
342
  catch (error) {