@happyvertical/smrt-content 0.43.4 → 0.43.6
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/AGENTS.md +9 -86
- package/agents/content-list.md +869 -0
- package/dist/content-query.d.ts +310 -0
- package/dist/content-query.d.ts.map +1 -0
- package/dist/contents.d.ts +22 -0
- package/dist/contents.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +672 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +22 -2
- package/dist/smrt-knowledge.json +38 -5
- package/dist/svelte/components/ContentList.svelte +941 -30
- package/dist/svelte/components/ContentList.svelte.d.ts +44 -1
- package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.d.ts +98 -1
- package/dist/svelte/content-list-controller.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.js +290 -19
- package/dist/svelte/content-list-query.d.ts +498 -0
- package/dist/svelte/content-list-query.d.ts.map +1 -0
- package/dist/svelte/content-list-query.js +1294 -0
- package/dist/svelte/content-list-saved-views.d.ts +172 -0
- package/dist/svelte/content-list-saved-views.d.ts.map +1 -0
- package/dist/svelte/content-list-saved-views.js +298 -0
- package/dist/svelte/content-list-url-state.d.ts +211 -0
- package/dist/svelte/content-list-url-state.d.ts.map +1 -0
- package/dist/svelte/content-list-url-state.js +856 -0
- package/dist/svelte/i18n.contribution.d.ts +24 -0
- package/dist/svelte/i18n.contribution.d.ts.map +1 -1
- package/dist/svelte/i18n.contribution.js +26 -0
- package/dist/svelte/index.d.ts +5 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +8 -1
- package/package.json +16 -15
|
@@ -1,9 +1,41 @@
|
|
|
1
|
+
import type { DataTableViewState as ContentListDataTableViewState } from '@happyvertical/smrt-ui/data';
|
|
2
|
+
import type { ContentListUrlStateOptions } from '../content-list-url-state.js';
|
|
3
|
+
/**
|
|
4
|
+
* Optional, router-agnostic URL state (#2452).
|
|
5
|
+
*
|
|
6
|
+
* `ContentList` never imports a router: it reads `params` once during
|
|
7
|
+
* initialization and hands the merged parameters back through `onChange`, so a
|
|
8
|
+
* SvelteKit host calls `replaceState`, a hash router rewrites the fragment, and
|
|
9
|
+
* a test passes a plain `URLSearchParams`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ContentListUrlStateBinding {
|
|
12
|
+
/**
|
|
13
|
+
* Query parameters to restore the view from. Read once, at initialization —
|
|
14
|
+
* later navigation is the host's to drive (re-key the component to re-read).
|
|
15
|
+
*/
|
|
16
|
+
params?: URLSearchParams | string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Receives the full merged parameter set — every foreign parameter
|
|
19
|
+
* preserved — whenever the query-affecting state changes. Never called for
|
|
20
|
+
* the initial restore.
|
|
21
|
+
*/
|
|
22
|
+
onChange?: (params: URLSearchParams, state: ContentListDataTableViewState) => void;
|
|
23
|
+
/** Prefix and default page size, forwarded to the URL-state module. */
|
|
24
|
+
options?: ContentListUrlStateOptions;
|
|
25
|
+
}
|
|
1
26
|
import type { Snippet } from 'svelte';
|
|
2
27
|
import type { ContentData } from '../../mock-smrt-client.js';
|
|
3
28
|
import { type ContentListDataSurface, type ContentListViewMode } from '../content-list-controller.js';
|
|
29
|
+
import { type ContentListQuerySource } from '../content-list-query.js';
|
|
30
|
+
import { type ContentListSavedViewStore } from '../content-list-saved-views.js';
|
|
4
31
|
interface Props {
|
|
5
32
|
apiBaseUrl?: string;
|
|
6
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Client-side rows. Ignored when `query` is supplied — the server then owns
|
|
35
|
+
* filtering, sorting, and paging, and these rows would be a second, disagreeing
|
|
36
|
+
* source of truth.
|
|
37
|
+
*/
|
|
38
|
+
contents?: ContentData[];
|
|
7
39
|
type?: string;
|
|
8
40
|
defaultViewMode?: ContentListViewMode;
|
|
9
41
|
onEdit: (content: ContentData) => void;
|
|
@@ -19,6 +51,17 @@ interface Props {
|
|
|
19
51
|
onRetry?: () => void;
|
|
20
52
|
/** Opt-in agent addressability. Non-table presentations land with #2456. */
|
|
21
53
|
dataSurface?: ContentListDataSurface;
|
|
54
|
+
/**
|
|
55
|
+
* Opt-in server-backed rows (#2452). `bind()` is called exactly once, during
|
|
56
|
+
* initialization, so a `remoteQuery(...)` binding is disposed with this
|
|
57
|
+
* component. Supplying it switches the list into server mode: `contents` is
|
|
58
|
+
* ignored and the local select/paginate transform never runs.
|
|
59
|
+
*/
|
|
60
|
+
query?: ContentListQuerySource;
|
|
61
|
+
/** Opt-in shareable URL state. The host owns navigation. */
|
|
62
|
+
urlState?: ContentListUrlStateBinding;
|
|
63
|
+
/** Opt-in saved views. `createContentListSavedViewStore()` is the default store. */
|
|
64
|
+
savedViews?: ContentListSavedViewStore;
|
|
22
65
|
}
|
|
23
66
|
declare const ContentList: import("svelte").Component<Props, {}, "">;
|
|
24
67
|
type ContentList = ReturnType<typeof ContentList>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,IAAI,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,6BAA6B,KACjC,IAAI,CAAC;IACV,uEAAuE;IACvE,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACtC;AAYD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EASL,KAAK,sBAAsB,EAE3B,KAAK,mBAAmB,EAiBzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAML,KAAK,sBAAsB,EAS5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,gCAAgC,CAAC;AAgBxC,UAAU,KAAK;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC;IACtD,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,oFAAoF;IACpF,UAAU,CAAC,EAAE,yBAAyB,CAAC;CACxC;AAg2CD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -123,6 +123,33 @@ export interface ContentListActionOptions {
|
|
|
123
123
|
export declare function normalizeContentType(value: unknown): string;
|
|
124
124
|
/** Normalizes a status/state token for filtering and badge variants. */
|
|
125
125
|
export declare function normalizeContentToken(value: unknown): string;
|
|
126
|
+
/**
|
|
127
|
+
* The type tokens the toolbar select offers.
|
|
128
|
+
*
|
|
129
|
+
* `Content.type` is freeform, so this is a display vocabulary rather than the
|
|
130
|
+
* model's domain: a value outside it is still a valid filter, and the list
|
|
131
|
+
* surfaces it rather than hiding it (see `ContentList`).
|
|
132
|
+
*/
|
|
133
|
+
export declare const CONTENT_LIST_TYPE_OPTIONS: readonly ["article", "document", "mirror"];
|
|
134
|
+
/**
|
|
135
|
+
* The status tokens the toolbar select offers.
|
|
136
|
+
*
|
|
137
|
+
* `Content.status` is `published | draft | review | archived | deleted`.
|
|
138
|
+
* `review` is offered because it is a real, reachable state that governance
|
|
139
|
+
* puts content into; omitting it meant `?status=review` restored a live
|
|
140
|
+
* predicate the toolbar could not show.
|
|
141
|
+
*
|
|
142
|
+
* `deleted` is deliberately NOT offered: it is the trash lifecycle, which is
|
|
143
|
+
* #2454's, and exposing it here would imply a restore/purge affordance this
|
|
144
|
+
* list does not have.
|
|
145
|
+
*/
|
|
146
|
+
export declare const CONTENT_LIST_STATUS_OPTIONS: readonly ["published", "draft", "review", "archived"];
|
|
147
|
+
/**
|
|
148
|
+
* Resolves the normalized type a `type` prop locks the list to, or `null` when
|
|
149
|
+
* the list is unlocked. Shared so the lock effect and the initial restore
|
|
150
|
+
* cannot disagree about what "locked" means.
|
|
151
|
+
*/
|
|
152
|
+
export declare function normalizeContentListTypeLock(value: string | null | undefined): string | null;
|
|
126
153
|
export declare function contentTypeLabel(value: unknown): string;
|
|
127
154
|
/** Badge variant for a status; unrecognized statuses degrade to `unknown`. */
|
|
128
155
|
export declare function contentStatusVariant(value: unknown): string;
|
|
@@ -153,9 +180,28 @@ export declare function resolveSelectedContents(rows: readonly ContentListRow[],
|
|
|
153
180
|
* searchable/filterable/sortable flags the descriptor is derived from.
|
|
154
181
|
*/
|
|
155
182
|
export declare function buildContentListColumns(labels?: ContentListColumnLabels): DataTableColumn<ContentListRow>[];
|
|
183
|
+
/**
|
|
184
|
+
* Columns whose stored values are lowercase tokens rather than free text.
|
|
185
|
+
*
|
|
186
|
+
* Only these may have their case normalized by a filter: their domain is a
|
|
187
|
+
* fixed vocabulary the model writes in lower case, so folding the operator's
|
|
188
|
+
* input to match it is a correction. Every other column holds text a person
|
|
189
|
+
* typed.
|
|
190
|
+
*/
|
|
191
|
+
export declare const CONTENT_LIST_TOKEN_COLUMN_IDS: readonly ["type", "status", "state"];
|
|
156
192
|
/**
|
|
157
193
|
* One normalizer per filter column, so a filter built by the toolbar, by the
|
|
158
194
|
* `type` lock, and by a restored view all compare equal.
|
|
195
|
+
*
|
|
196
|
+
* CASE IS PRESERVED FOR FREE TEXT. This helper was written for #2451, when
|
|
197
|
+
* every comparison happened in the browser and lowercasing everything was
|
|
198
|
+
* harmless. Under #2452 a stored filter value becomes a server-side `eq` or
|
|
199
|
+
* `like` predicate compared against the STORED text, so lowercasing `NASA`
|
|
200
|
+
* would send `%nasa%` and miss `NASA Update` on a case-sensitive backend
|
|
201
|
+
* (PostgreSQL, DuckDB). Local matching is unaffected either way: the local
|
|
202
|
+
* evaluator compares through `textValue()`, which lower-cases BOTH sides at
|
|
203
|
+
* compare time, so a case-preserving stored value still matches
|
|
204
|
+
* case-insensitively there.
|
|
159
205
|
*/
|
|
160
206
|
export declare function normalizeContentListFilterValue(columnId: string, value: string): string;
|
|
161
207
|
/** Builds the declarative filter set for the two toolbar filters. */
|
|
@@ -163,8 +209,59 @@ export declare function contentListFilters(values: {
|
|
|
163
209
|
type?: string | null;
|
|
164
210
|
status?: string | null;
|
|
165
211
|
}): DataTableFilter[];
|
|
166
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Reads one filter's value, or `null` when the filter is not applied.
|
|
214
|
+
*
|
|
215
|
+
* Value-only, and therefore NOT enough to drive a single-select toolbar
|
|
216
|
+
* control: it reports the same string for `equals 'draft'` and
|
|
217
|
+
* `notEquals 'draft'`. Use {@link readContentListSelectFilter} for anything
|
|
218
|
+
* that displays the filter to an operator.
|
|
219
|
+
*/
|
|
167
220
|
export declare function readContentListFilter(state: DataTableViewState, columnId: string): string | null;
|
|
221
|
+
/**
|
|
222
|
+
* The value a toolbar select carries while a live filter cannot be represented
|
|
223
|
+
* as one of its options. It is only ever set programmatically on a disabled
|
|
224
|
+
* option, so it can never be submitted; `applyContentListFilter` would replace
|
|
225
|
+
* every filter on the column anyway.
|
|
226
|
+
*
|
|
227
|
+
* The prefix is U+001F rather than U+0000 deliberately. The HTML tokenizer
|
|
228
|
+
* rewrites a NUL inside an attribute value to U+FFFD, so a server-rendered
|
|
229
|
+
* option would come back with a different value than the select was given and
|
|
230
|
+
* hydrate to no selection at all — exactly the state this sentinel exists to
|
|
231
|
+
* prevent. U+001F passes through attribute parsing unchanged.
|
|
232
|
+
*
|
|
233
|
+
* A crafted filter value could still equal it — the normalizer only trims and
|
|
234
|
+
* lower-cases — and that is harmless: the representable and unrepresentable
|
|
235
|
+
* paths are mutually exclusive, and the representable one renders the value as
|
|
236
|
+
* a real, enabled option carrying the same value.
|
|
237
|
+
*/
|
|
238
|
+
export declare const CONTENT_LIST_UNREPRESENTABLE_OPTION = "\u001Funrepresentable";
|
|
239
|
+
/** What a single-select toolbar control can honestly display for one column. */
|
|
240
|
+
export interface ContentListSelectFilterState {
|
|
241
|
+
/** The value to display, or `''` when there is nothing to display. */
|
|
242
|
+
value: string;
|
|
243
|
+
/**
|
|
244
|
+
* False when a live filter exists on this column that a single select cannot
|
|
245
|
+
* express — a non-`equals` operator, a list value, a valueless operator, or
|
|
246
|
+
* more than one filter. The caller must then not render a plain value: it
|
|
247
|
+
* would state something other than the query being run.
|
|
248
|
+
*/
|
|
249
|
+
representable: boolean;
|
|
250
|
+
/** A short, operator-facing summary of the live predicate when it is not. */
|
|
251
|
+
detail: string | null;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Resolves what a toolbar select may show for one column.
|
|
255
|
+
*
|
|
256
|
+
* A select offers a single `equals` value, but the filter vocabulary a link or
|
|
257
|
+
* a saved view can restore is much wider. `?status.in=draft,review` and
|
|
258
|
+
* `?status.isNull=1` constrain the query while a value-only read reports
|
|
259
|
+
* nothing, and `?status.notEquals=draft` reports `draft` — the exact inverse of
|
|
260
|
+
* what is being applied. This is the seam that keeps the control from
|
|
261
|
+
* misstating the query: either it can show the predicate exactly, or the caller
|
|
262
|
+
* is told it cannot and reports it.
|
|
263
|
+
*/
|
|
264
|
+
export declare function readContentListSelectFilter(state: DataTableViewState, columnId: string): ContentListSelectFilterState;
|
|
168
265
|
/**
|
|
169
266
|
* True when a column is filtered to exactly the given value and nothing else.
|
|
170
267
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-list-controller.d.ts","sourceRoot":"","sources":["../../src/svelte/content-list-controller.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"content-list-controller.d.ts","sourceRoot":"","sources":["../../src/svelte/content-list-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -126,6 +126,44 @@ export function normalizeContentType(value) {
|
|
|
126
126
|
export function normalizeContentToken(value) {
|
|
127
127
|
return getTextValue(value).trim().toLowerCase();
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The type tokens the toolbar select offers.
|
|
131
|
+
*
|
|
132
|
+
* `Content.type` is freeform, so this is a display vocabulary rather than the
|
|
133
|
+
* model's domain: a value outside it is still a valid filter, and the list
|
|
134
|
+
* surfaces it rather than hiding it (see `ContentList`).
|
|
135
|
+
*/
|
|
136
|
+
export const CONTENT_LIST_TYPE_OPTIONS = [
|
|
137
|
+
'article',
|
|
138
|
+
'document',
|
|
139
|
+
'mirror',
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* The status tokens the toolbar select offers.
|
|
143
|
+
*
|
|
144
|
+
* `Content.status` is `published | draft | review | archived | deleted`.
|
|
145
|
+
* `review` is offered because it is a real, reachable state that governance
|
|
146
|
+
* puts content into; omitting it meant `?status=review` restored a live
|
|
147
|
+
* predicate the toolbar could not show.
|
|
148
|
+
*
|
|
149
|
+
* `deleted` is deliberately NOT offered: it is the trash lifecycle, which is
|
|
150
|
+
* #2454's, and exposing it here would imply a restore/purge affordance this
|
|
151
|
+
* list does not have.
|
|
152
|
+
*/
|
|
153
|
+
export const CONTENT_LIST_STATUS_OPTIONS = [
|
|
154
|
+
'published',
|
|
155
|
+
'draft',
|
|
156
|
+
'review',
|
|
157
|
+
'archived',
|
|
158
|
+
];
|
|
159
|
+
/**
|
|
160
|
+
* Resolves the normalized type a `type` prop locks the list to, or `null` when
|
|
161
|
+
* the list is unlocked. Shared so the lock effect and the initial restore
|
|
162
|
+
* cannot disagree about what "locked" means.
|
|
163
|
+
*/
|
|
164
|
+
export function normalizeContentListTypeLock(value) {
|
|
165
|
+
return value?.trim() ? normalizeContentType(value) : null;
|
|
166
|
+
}
|
|
129
167
|
export function contentTypeLabel(value) {
|
|
130
168
|
switch (normalizeContentType(value)) {
|
|
131
169
|
case 'article':
|
|
@@ -322,14 +360,40 @@ export function buildContentListColumns(labels = {}) {
|
|
|
322
360
|
},
|
|
323
361
|
];
|
|
324
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Columns whose stored values are lowercase tokens rather than free text.
|
|
365
|
+
*
|
|
366
|
+
* Only these may have their case normalized by a filter: their domain is a
|
|
367
|
+
* fixed vocabulary the model writes in lower case, so folding the operator's
|
|
368
|
+
* input to match it is a correction. Every other column holds text a person
|
|
369
|
+
* typed.
|
|
370
|
+
*/
|
|
371
|
+
export const CONTENT_LIST_TOKEN_COLUMN_IDS = [
|
|
372
|
+
'type',
|
|
373
|
+
'status',
|
|
374
|
+
'state',
|
|
375
|
+
];
|
|
376
|
+
const TOKEN_COLUMNS = new Set(CONTENT_LIST_TOKEN_COLUMN_IDS);
|
|
325
377
|
/**
|
|
326
378
|
* One normalizer per filter column, so a filter built by the toolbar, by the
|
|
327
379
|
* `type` lock, and by a restored view all compare equal.
|
|
380
|
+
*
|
|
381
|
+
* CASE IS PRESERVED FOR FREE TEXT. This helper was written for #2451, when
|
|
382
|
+
* every comparison happened in the browser and lowercasing everything was
|
|
383
|
+
* harmless. Under #2452 a stored filter value becomes a server-side `eq` or
|
|
384
|
+
* `like` predicate compared against the STORED text, so lowercasing `NASA`
|
|
385
|
+
* would send `%nasa%` and miss `NASA Update` on a case-sensitive backend
|
|
386
|
+
* (PostgreSQL, DuckDB). Local matching is unaffected either way: the local
|
|
387
|
+
* evaluator compares through `textValue()`, which lower-cases BOTH sides at
|
|
388
|
+
* compare time, so a case-preserving stored value still matches
|
|
389
|
+
* case-insensitively there.
|
|
328
390
|
*/
|
|
329
391
|
export function normalizeContentListFilterValue(columnId, value) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
392
|
+
if (columnId === CONTENT_LIST_TYPE_FILTER_ID)
|
|
393
|
+
return normalizeContentType(value);
|
|
394
|
+
if (TOKEN_COLUMNS.has(columnId))
|
|
395
|
+
return normalizeContentToken(value);
|
|
396
|
+
return value.trim();
|
|
333
397
|
}
|
|
334
398
|
/** Builds the declarative filter set for the two toolbar filters. */
|
|
335
399
|
export function contentListFilters(values) {
|
|
@@ -350,11 +414,73 @@ export function contentListFilters(values) {
|
|
|
350
414
|
}
|
|
351
415
|
return filters;
|
|
352
416
|
}
|
|
353
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Reads one filter's value, or `null` when the filter is not applied.
|
|
419
|
+
*
|
|
420
|
+
* Value-only, and therefore NOT enough to drive a single-select toolbar
|
|
421
|
+
* control: it reports the same string for `equals 'draft'` and
|
|
422
|
+
* `notEquals 'draft'`. Use {@link readContentListSelectFilter} for anything
|
|
423
|
+
* that displays the filter to an operator.
|
|
424
|
+
*/
|
|
354
425
|
export function readContentListFilter(state, columnId) {
|
|
355
426
|
const filter = state.filters.find((candidate) => candidate.columnId === columnId);
|
|
356
427
|
return typeof filter?.value === 'string' ? filter.value : null;
|
|
357
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* The value a toolbar select carries while a live filter cannot be represented
|
|
431
|
+
* as one of its options. It is only ever set programmatically on a disabled
|
|
432
|
+
* option, so it can never be submitted; `applyContentListFilter` would replace
|
|
433
|
+
* every filter on the column anyway.
|
|
434
|
+
*
|
|
435
|
+
* The prefix is U+001F rather than U+0000 deliberately. The HTML tokenizer
|
|
436
|
+
* rewrites a NUL inside an attribute value to U+FFFD, so a server-rendered
|
|
437
|
+
* option would come back with a different value than the select was given and
|
|
438
|
+
* hydrate to no selection at all — exactly the state this sentinel exists to
|
|
439
|
+
* prevent. U+001F passes through attribute parsing unchanged.
|
|
440
|
+
*
|
|
441
|
+
* A crafted filter value could still equal it — the normalizer only trims and
|
|
442
|
+
* lower-cases — and that is harmless: the representable and unrepresentable
|
|
443
|
+
* paths are mutually exclusive, and the representable one renders the value as
|
|
444
|
+
* a real, enabled option carrying the same value.
|
|
445
|
+
*/
|
|
446
|
+
export const CONTENT_LIST_UNREPRESENTABLE_OPTION = '\u001Funrepresentable';
|
|
447
|
+
function describeContentListFilter(filter) {
|
|
448
|
+
if (filter.value === undefined)
|
|
449
|
+
return filter.operator;
|
|
450
|
+
const value = Array.isArray(filter.value)
|
|
451
|
+
? filter.value.map(String).join(', ')
|
|
452
|
+
: String(filter.value);
|
|
453
|
+
return `${filter.operator} ${value}`;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Resolves what a toolbar select may show for one column.
|
|
457
|
+
*
|
|
458
|
+
* A select offers a single `equals` value, but the filter vocabulary a link or
|
|
459
|
+
* a saved view can restore is much wider. `?status.in=draft,review` and
|
|
460
|
+
* `?status.isNull=1` constrain the query while a value-only read reports
|
|
461
|
+
* nothing, and `?status.notEquals=draft` reports `draft` — the exact inverse of
|
|
462
|
+
* what is being applied. This is the seam that keeps the control from
|
|
463
|
+
* misstating the query: either it can show the predicate exactly, or the caller
|
|
464
|
+
* is told it cannot and reports it.
|
|
465
|
+
*/
|
|
466
|
+
export function readContentListSelectFilter(state, columnId) {
|
|
467
|
+
const applied = state.filters.filter((candidate) => candidate.columnId === columnId);
|
|
468
|
+
if (applied.length === 0) {
|
|
469
|
+
return { value: '', representable: true, detail: null };
|
|
470
|
+
}
|
|
471
|
+
const unrepresentable = () => ({
|
|
472
|
+
value: '',
|
|
473
|
+
representable: false,
|
|
474
|
+
detail: applied.map(describeContentListFilter).join('; '),
|
|
475
|
+
});
|
|
476
|
+
if (applied.length > 1)
|
|
477
|
+
return unrepresentable();
|
|
478
|
+
const [filter] = applied;
|
|
479
|
+
if (filter.operator !== 'equals' || typeof filter.value !== 'string') {
|
|
480
|
+
return unrepresentable();
|
|
481
|
+
}
|
|
482
|
+
return { value: filter.value, representable: true, detail: null };
|
|
483
|
+
}
|
|
358
484
|
/**
|
|
359
485
|
* True when a column is filtered to exactly the given value and nothing else.
|
|
360
486
|
*
|
|
@@ -441,23 +567,143 @@ function compareFilterValues(left, right) {
|
|
|
441
567
|
const rightText = textValue(right);
|
|
442
568
|
return leftText === rightText ? 0 : leftText < rightText ? -1 : 1;
|
|
443
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* True when the content behind a row carries no value at all for a column.
|
|
572
|
+
*
|
|
573
|
+
* `ContentListRow` flattens every field to display text, so an absent value and
|
|
574
|
+
* an empty one both read as `''` — which made every ordered comparison treat
|
|
575
|
+
* "no author" as the smallest possible author, and made `isNull` match nothing.
|
|
576
|
+
* The original `ContentData` still distinguishes them, so the null-sensitive
|
|
577
|
+
* operators consult it. A derived column such as `site` has no single source
|
|
578
|
+
* field and is never absent.
|
|
579
|
+
*/
|
|
580
|
+
function isAbsentContentValue(row, column) {
|
|
581
|
+
const fieldName = CONTENT_LIST_COLUMN_FIELD_NAMES[column.id];
|
|
582
|
+
if (!fieldName)
|
|
583
|
+
return false;
|
|
584
|
+
const source = row.content[fieldName];
|
|
585
|
+
return source === null || source === undefined;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Columns whose flattened text is a LABEL when the content carries no value:
|
|
589
|
+
* `type` reads `content` and `title` reads `Untitled content`. Every other
|
|
590
|
+
* column flattens to empty text, which is what the stored column holds.
|
|
591
|
+
*/
|
|
592
|
+
const CONTENT_LIST_LABEL_FALLBACK_COLUMNS = new Set(['type', 'title']);
|
|
593
|
+
/**
|
|
594
|
+
* The value a local comparison reads for one column.
|
|
595
|
+
*
|
|
596
|
+
* A fallback label is presentation, not data. Comparing it makes
|
|
597
|
+
* `?type=content` match every untyped row locally and none server-side, and
|
|
598
|
+
* makes a search for `untitled` match rows whose title is simply missing — the
|
|
599
|
+
* same link returning different data depending on how the host configured the
|
|
600
|
+
* list. For those two columns the comparison reads what is stored: `null` when
|
|
601
|
+
* the content has no value at all (which compares false against everything,
|
|
602
|
+
* exactly as SQL does), empty text when the value is genuinely blank, and
|
|
603
|
+
* otherwise the flattened text, which is already faithful.
|
|
604
|
+
*/
|
|
605
|
+
function comparisonValue(row, column) {
|
|
606
|
+
const value = getNestedValue(row, String(column.accessor ?? column.id));
|
|
607
|
+
if (!CONTENT_LIST_LABEL_FALLBACK_COLUMNS.has(column.id))
|
|
608
|
+
return value;
|
|
609
|
+
const fieldName = CONTENT_LIST_COLUMN_FIELD_NAMES[column.id];
|
|
610
|
+
const source = fieldName
|
|
611
|
+
? row.content[fieldName]
|
|
612
|
+
: undefined;
|
|
613
|
+
if (source === null || source === undefined)
|
|
614
|
+
return null;
|
|
615
|
+
if (typeof source === 'string' && source.trim() === '')
|
|
616
|
+
return '';
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
444
619
|
/**
|
|
445
620
|
* The single declarative-filter evaluator. It follows DataTable's operator
|
|
446
621
|
* semantics so a persisted or agent-issued filter behaves the same here as it
|
|
447
622
|
* would in a locally filtered table.
|
|
623
|
+
*
|
|
624
|
+
* The null-sensitive operators (`gt`/`gte`/`lt`/`lte`, `isNull`/`isNotNull`)
|
|
625
|
+
* additionally agree with SQL, and no comparison reads a display fallback, so
|
|
626
|
+
* the same shared link means the same thing on a client-array list and a
|
|
627
|
+
* server-backed one (#2452).
|
|
448
628
|
*/
|
|
629
|
+
/**
|
|
630
|
+
* What an ABSENT value does to each operator, as the executor's SQL decides it.
|
|
631
|
+
*
|
|
632
|
+
* The flattened row reads an absent value as empty text, so comparing it as
|
|
633
|
+
* text answers a question about `''` rather than about absence — and the two
|
|
634
|
+
* differ for every operator once a BLANK comparand is involved. `author equals
|
|
635
|
+
* ''` matches an absent row as text and no row in SQL; `author notEquals ''`
|
|
636
|
+
* does the reverse. Deciding from the operator alone removes the text
|
|
637
|
+
* comparison from the picture entirely.
|
|
638
|
+
*
|
|
639
|
+
* The table is the SQL the executor emits (see `conditionToDnf`), not raw
|
|
640
|
+
* three-valued logic: `ne`/`notIn` union `IS NULL` unless a `null` is listed,
|
|
641
|
+
* which is why they read the way they do here.
|
|
642
|
+
*/
|
|
643
|
+
function matchesAbsentContentValue(filter) {
|
|
644
|
+
const listsNull = Array.isArray(filter.value) && filter.value.some((entry) => entry === null);
|
|
645
|
+
switch (filter.operator) {
|
|
646
|
+
// `eq v` is `= v`, true only when the caller named absence itself.
|
|
647
|
+
case 'equals':
|
|
648
|
+
return filter.value === null;
|
|
649
|
+
// `ne v` is `IS NULL OR <> v`; `ne null` is `IS NOT NULL`.
|
|
650
|
+
case 'notEquals':
|
|
651
|
+
return filter.value !== null;
|
|
652
|
+
// `in` matches an absent row only when the list carries a `null`.
|
|
653
|
+
case 'in':
|
|
654
|
+
return listsNull;
|
|
655
|
+
// `notIn` unions `IS NULL` unless a `null` is listed, which excludes it.
|
|
656
|
+
case 'notIn':
|
|
657
|
+
return !listsNull;
|
|
658
|
+
case 'isNull':
|
|
659
|
+
return true;
|
|
660
|
+
case 'isNotNull':
|
|
661
|
+
return false;
|
|
662
|
+
// `like` and every ordered comparison are UNKNOWN for NULL, so no row with
|
|
663
|
+
// no value takes part in one.
|
|
664
|
+
case 'contains':
|
|
665
|
+
case 'startsWith':
|
|
666
|
+
case 'endsWith':
|
|
667
|
+
case 'gt':
|
|
668
|
+
case 'gte':
|
|
669
|
+
case 'lt':
|
|
670
|
+
case 'lte':
|
|
671
|
+
return false;
|
|
672
|
+
// `notContains` has no server form — the executor refuses a negated
|
|
673
|
+
// `like` and the translator drops it — so there is nothing to align to.
|
|
674
|
+
// Keep the local set-complement reading.
|
|
675
|
+
case 'notContains':
|
|
676
|
+
return true;
|
|
677
|
+
default:
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
449
681
|
function matchesContentListFilter(row, column, filter) {
|
|
450
682
|
if (column.filterable === false)
|
|
451
683
|
return true;
|
|
452
|
-
|
|
684
|
+
// Absence is decided before any text comparison, because the flattened row
|
|
685
|
+
// cannot represent it. The arms below stay correct for a value that is
|
|
686
|
+
// present; none of them can see an absent one.
|
|
687
|
+
if (isAbsentContentValue(row, column)) {
|
|
688
|
+
return matchesAbsentContentValue(filter);
|
|
689
|
+
}
|
|
690
|
+
const value = comparisonValue(row, column);
|
|
453
691
|
const expected = filter.value;
|
|
454
692
|
const valueText = textValue(value);
|
|
455
693
|
const expectedText = textValue(expected);
|
|
694
|
+
// A literal `null` in a filter names ABSENCE, and the flattened row cannot
|
|
695
|
+
// express it — an absent author reads as empty text, which is not the same
|
|
696
|
+
// thing. `in`/`notIn` lists reach here from a data-surface `set-filters`
|
|
697
|
+
// command, and the executor lowers a listed null to `IS NULL` / `IS NOT
|
|
698
|
+
// NULL`; matching that here is what keeps the two modes agreeing.
|
|
699
|
+
const matchesEntry = (entry) => entry === null
|
|
700
|
+
? isAbsentContentValue(row, column)
|
|
701
|
+
: sameFilterValue(value, entry);
|
|
456
702
|
switch (filter.operator) {
|
|
457
703
|
case 'equals':
|
|
458
|
-
return
|
|
704
|
+
return matchesEntry(expected);
|
|
459
705
|
case 'notEquals':
|
|
460
|
-
return !
|
|
706
|
+
return !matchesEntry(expected);
|
|
461
707
|
case 'contains':
|
|
462
708
|
return valueText.includes(expectedText);
|
|
463
709
|
case 'notContains':
|
|
@@ -467,23 +713,36 @@ function matchesContentListFilter(row, column, filter) {
|
|
|
467
713
|
case 'endsWith':
|
|
468
714
|
return valueText.endsWith(expectedText);
|
|
469
715
|
case 'in':
|
|
470
|
-
return
|
|
471
|
-
expected.some((entry) => sameFilterValue(value, entry)));
|
|
716
|
+
return Array.isArray(expected) && expected.some(matchesEntry);
|
|
472
717
|
case 'notIn':
|
|
473
|
-
return
|
|
474
|
-
!expected.some((entry) => sameFilterValue(value, entry)));
|
|
718
|
+
return Array.isArray(expected) && !expected.some(matchesEntry);
|
|
475
719
|
case 'gt':
|
|
476
|
-
return compareFilterValues(value, expected) > 0;
|
|
477
720
|
case 'gte':
|
|
478
|
-
return compareFilterValues(value, expected) >= 0;
|
|
479
721
|
case 'lt':
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
722
|
+
case 'lte': {
|
|
723
|
+
// An absent value takes part in no ordered comparison, exactly as SQL
|
|
724
|
+
// yields UNKNOWN for NULL. Without this the flattened empty string sorts
|
|
725
|
+
// below everything, so `publish_date lt X` would match every row that was
|
|
726
|
+
// never published — and disagree with the server for the same link.
|
|
727
|
+
if (isAbsentContentValue(row, column))
|
|
728
|
+
return false;
|
|
729
|
+
const comparison = compareFilterValues(value, expected);
|
|
730
|
+
if (filter.operator === 'gt')
|
|
731
|
+
return comparison > 0;
|
|
732
|
+
if (filter.operator === 'gte')
|
|
733
|
+
return comparison >= 0;
|
|
734
|
+
if (filter.operator === 'lt')
|
|
735
|
+
return comparison < 0;
|
|
736
|
+
return comparison <= 0;
|
|
737
|
+
}
|
|
483
738
|
case 'isNull':
|
|
484
|
-
return
|
|
739
|
+
return (isAbsentContentValue(row, column) ||
|
|
740
|
+
value === null ||
|
|
741
|
+
value === undefined);
|
|
485
742
|
case 'isNotNull':
|
|
486
|
-
return
|
|
743
|
+
return !(isAbsentContentValue(row, column) ||
|
|
744
|
+
value === null ||
|
|
745
|
+
value === undefined);
|
|
487
746
|
default:
|
|
488
747
|
return false;
|
|
489
748
|
}
|
|
@@ -498,7 +757,7 @@ export function selectContentListRows(rows, state, columns = buildContentListCol
|
|
|
498
757
|
const filtered = rows.filter((row) => {
|
|
499
758
|
if (search &&
|
|
500
759
|
!columns.some((column) => column.searchable !== false &&
|
|
501
|
-
textValue(
|
|
760
|
+
textValue(comparisonValue(row, column)).includes(search))) {
|
|
502
761
|
return false;
|
|
503
762
|
}
|
|
504
763
|
return state.filters.every((filter) => {
|
|
@@ -513,6 +772,18 @@ export function selectContentListRows(rows, state, columns = buildContentListCol
|
|
|
513
772
|
const column = columns.find((candidate) => candidate.id === rule.columnId);
|
|
514
773
|
if (!column)
|
|
515
774
|
continue;
|
|
775
|
+
// Absent values sort LAST ascending and FIRST descending. Without this
|
|
776
|
+
// an absent value flattens to empty text and sorts first ascending —
|
|
777
|
+
// which silently changes which rows land on page one when the same list
|
|
778
|
+
// is served from the query endpoint. See `agents/content-list.md`: the
|
|
779
|
+
// placement matches the SQL standard and the PostgreSQL/DuckDB default,
|
|
780
|
+
// and no portable way exists to state it in an `orderBy` term.
|
|
781
|
+
const leftAbsent = isAbsentContentValue(left, column);
|
|
782
|
+
const rightAbsent = isAbsentContentValue(right, column);
|
|
783
|
+
if (leftAbsent !== rightAbsent) {
|
|
784
|
+
const absentAfter = leftAbsent ? 1 : -1;
|
|
785
|
+
return rule.direction === 'desc' ? -absentAfter : absentAfter;
|
|
786
|
+
}
|
|
516
787
|
const result = defaultSort(left, right, String(column.accessor ?? column.id), rule.direction);
|
|
517
788
|
if (result !== 0)
|
|
518
789
|
return result;
|