@conduction/nextcloud-vue 2.103.0 → 2.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +159 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +105 -85
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +7 -7
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +114 -33
- package/dist/esm/utils/nextcloudDirectory.js +18 -1
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +61 -0
- package/dist/esm/utils/sortOrder.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4172 -3662
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +114 -33
- package/l10n/en.json +4 -0
- package/l10n/nl.json +4 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
- package/src/components/CnAppRoot/CnAppRoot.md +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +49 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
- package/src/components/CnIndexPage/CnIndexPage.md +4 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +174 -8
- package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
- package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
- package/src/composables/indexSources.js +7 -7
- package/src/css/index-page.css +19 -0
- package/src/css/look-board-index.css +48 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -1
- package/src/utils/nextcloudDirectory.js +17 -0
- package/src/utils/sortOrder.js +58 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/src/css/index-page.css
CHANGED
|
@@ -190,6 +190,25 @@
|
|
|
190
190
|
font-size: 14px;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* The selected folder's heading above the list. */
|
|
194
|
+
.cn-index-page__scope-heading {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
gap: 4px;
|
|
198
|
+
margin-bottom: 12px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.cn-index-page__scope-title {
|
|
202
|
+
margin: 0;
|
|
203
|
+
font-size: 1.25em;
|
|
204
|
+
font-weight: 700;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.cn-index-page__scope-subtitle {
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
color: var(--color-text-maxcontrast);
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
/* Opt-in folder navigation pane (left of the list). */
|
|
194
213
|
.cn-index-page__folder-pane {
|
|
195
214
|
flex: 0 0 240px;
|
|
@@ -372,6 +372,17 @@
|
|
|
372
372
|
cursor: not-allowed;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/* `searchPlacement: "toolbar"`: the search field on row 1 takes the room
|
|
376
|
+
before Filter and the view switch, which keep the end of the row. */
|
|
377
|
+
.cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar {
|
|
378
|
+
flex: 1 1 220px;
|
|
379
|
+
min-width: 180px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar ~ .cn-actions-bar__view-controls {
|
|
383
|
+
margin-inline-start: 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
375
386
|
/* The search field: 40px, radius 8, not a pill. */
|
|
376
387
|
.cn-look-board .cn-actions-bar--board .cn-actions-bar__search {
|
|
377
388
|
flex: 1 1 260px;
|
|
@@ -1108,3 +1119,40 @@
|
|
|
1108
1119
|
.cn-look-board:has(.cn-index-page) > .app-sidebar__toggle {
|
|
1109
1120
|
display: none;
|
|
1110
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
/* The folder pane as a card (DqAfdelingen): 280px, a hairline border, 12px
|
|
1124
|
+
corners, with each folder's count in a grey pill. */
|
|
1125
|
+
.cn-look-board .cn-index-page__folder-pane {
|
|
1126
|
+
flex: 0 0 280px;
|
|
1127
|
+
align-self: flex-start;
|
|
1128
|
+
margin-inline-end: 20px;
|
|
1129
|
+
padding: 14px;
|
|
1130
|
+
border: 1px solid var(--cn-board-hairline);
|
|
1131
|
+
border-radius: var(--cn-board-card-radius, 12px);
|
|
1132
|
+
background: var(--color-main-background);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.cn-look-board .cn-index-page__folder-pane .cn-folder-tree__count {
|
|
1136
|
+
display: inline-flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
justify-content: center;
|
|
1139
|
+
min-width: 20px;
|
|
1140
|
+
height: 20px;
|
|
1141
|
+
padding: 0 6px;
|
|
1142
|
+
box-sizing: border-box;
|
|
1143
|
+
border-radius: 10px;
|
|
1144
|
+
background: var(--color-background-dark);
|
|
1145
|
+
color: var(--color-main-text);
|
|
1146
|
+
font-size: 12px;
|
|
1147
|
+
font-weight: 600;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* The selected folder's heading above the list: a 20px title over a soft line. */
|
|
1151
|
+
.cn-look-board .cn-index-page__scope-heading {
|
|
1152
|
+
margin-bottom: 16px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.cn-look-board .cn-index-page__scope-title {
|
|
1156
|
+
font-size: 20px;
|
|
1157
|
+
font-weight: 700;
|
|
1158
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.127.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1998,6 +1998,7 @@
|
|
|
1998
1998
|
"items": { "$ref": "#/$defs/action" },
|
|
1999
1999
|
"description": "Declarative row/widget actions rendered by CnWidgetObjectTable. object-op patch/delete render per row; object-op create renders as a widget-scoped footer affordance."
|
|
2000
2000
|
},
|
|
2001
|
+
"pageSize": { "type": "integer", "minimum": 1, "description": "Rows per page: the table shows one page with the board pager under it (\"4 of 7\", DqCatalogus). Under a pager `source.limit` is how many rows are read (2.127.0)." },
|
|
2001
2002
|
"rowClass": {
|
|
2002
2003
|
"type": "array",
|
|
2003
2004
|
"description": "#91: declarative per-row CSS class rules compiled into CnDataTable's rowClass function. Each rule { when: { field, op?, value }, class } adds `class` to a row when the shared visibleWhen predicate holds against that row (field = dot-path into the row, op = eq|neq|gt|gte|lt|lte default eq, value = literal). Rules evaluate in order and every matching class is space-joined — so overdue / at-risk rows can be highlighted from the manifest. A host-supplied rowClass FUNCTION (runtime-only) still passes straight through.",
|
|
@@ -4052,6 +4053,8 @@
|
|
|
4052
4053
|
"additionalProperties": true,
|
|
4053
4054
|
"description": "type='index': the opt-in folder navigation pane (CnFolderSidebar) rendered left of the list. Selecting a folder filters the list by `filterField` (or `field`); \"All\" clears it. Only the per-scope list-layout keys are typed here; the pane's own keys (source, register, schema, idField, nameField, folders, allLabel, title, allowCreate, ...) stay free-form. Maps to CnIndexPage `folderSidebar`.",
|
|
4054
4055
|
"properties": {
|
|
4056
|
+
"heading": { "type": "boolean", "description": "true heads the list with the selected folder's name, its parent folder and its count (always on a `groups` pane, 2.127.0)." },
|
|
4057
|
+
"countText": { "type": "string", "minLength": 1, "description": "The count in that heading, `{count}` filled in, through the app's translate; a groups pane defaults to \"{count} members\" (2.127.0)." },
|
|
4055
4058
|
"pane": { "type": "boolean", "default": true, "description": "false keeps the folder scope (a deep link such as `?caseType=<uuid>` still applies the scope's look and filter) without drawing the pane (dossiq's simple profile, 2.126.0)." },
|
|
4056
4059
|
"folders": {
|
|
4057
4060
|
"type": "array",
|
|
@@ -4215,6 +4218,11 @@
|
|
|
4215
4218
|
]
|
|
4216
4219
|
}
|
|
4217
4220
|
},
|
|
4221
|
+
"sideColumnFrom": {
|
|
4222
|
+
"type": "string",
|
|
4223
|
+
"minLength": 1,
|
|
4224
|
+
"description": "type='detail': a field path, read like a `visibleWhen` field, whose value picks the side column and its order for this record, e.g. `@context.caseType.sideCards` (a case type that names the cards its cases show). When the value is a non-empty list of widget ids, the side column is those of `sideColumn` in that order and the rest are left out; each still follows its own `visibleWhen`. Any other value, or no value, keeps `sideColumn` as declared. Maps to CnDetailPage `sideColumnFrom`."
|
|
4225
|
+
},
|
|
4218
4226
|
"headerActions": {
|
|
4219
4227
|
"type": "array",
|
|
4220
4228
|
"description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. See manifest-icons-and-page-actions for the dispatch semantics.",
|
|
@@ -4714,10 +4722,24 @@
|
|
|
4714
4722
|
"variant": { "type": "string", "enum": ["primary", "secondary"], "default": "secondary" },
|
|
4715
4723
|
"icon": { "type": "string", "minLength": 1, "description": "MDI icon name." },
|
|
4716
4724
|
"format": { "type": "string", "enum": ["csv", "excel"], "default": "csv", "description": "For `export`." },
|
|
4725
|
+
"run": { "type": "string", "minLength": 1, "description": "For `export`: the id of a `headerActions` entry the button runs instead of the export dialog. On a named or endpoint source an Export without `run` (and without a mass-export listener) is not drawn (2.127.0)." },
|
|
4717
4726
|
"items": { "type": "array", "minItems": 1, "items": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["heading"], "properties": { "heading": { "type": "string", "minLength": 1, "description": "i18n key of a group heading." } } } ] }, "description": "For `actions-menu`: the menu's entries, in order. Each is `header-actions` (every header action), the id of one `headerActions` entry, or a built-in of the actions bar's own menu, which the bar drops while header buttons show: `import`, `export`, `refresh`, `copy-selected`, `delete-selected` (disabled until a row is selected) or `documentation` (a link to the page's documentationUrl, left out without one). An unknown name is left out. Absent: the header actions only (screens-index-header-actions-menu, 2.90.0). Entries can be grouped: `{ \"heading\": \"<i18n key>\" }` draws a group heading and `\"separator\"` a rule, each only when an entry follows it, so a group whose entries are all left out leaves no empty heading (screens-index-header-menu-headings, 2.93.0)." }
|
|
4718
4727
|
}
|
|
4719
4728
|
}
|
|
4720
4729
|
},
|
|
4730
|
+
"search": { "type": "boolean", "default": true, "description": "type='index': `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page. Wins over `inlineSearch` (2.127.0)." },
|
|
4731
|
+
"searchPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index', board look: where the search field sits. `toolbar` puts it on row 1 before the Filter button and the view switch instead of opening row 2 (DqTeamwachtrij, 2.127.0)." },
|
|
4732
|
+
"defaultOrder": { "description": "type='index', self-fetch: the order the page opens on when neither the address nor sortKeys gives one: `\"-createdAt\"`, `\"priority:desc,title\"`, an `_order` map or a `[{ key, order }]` list (2.127.0).", "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] } }, { "type": "array", "items": { "type": "object", "required": ["key"], "properties": { "key": { "type": "string", "minLength": 1 }, "order": { "type": "string", "enum": ["asc", "desc"] } } } } ] },
|
|
4733
|
+
"listConfig": {
|
|
4734
|
+
"type": "object",
|
|
4735
|
+
"additionalProperties": true,
|
|
4736
|
+
"description": "type='index': the field mapping of the list view's rows (CnObjectRow): titleField, subtitleField, imageField, iconField, iconName, badgeField and the badge keys stay free-form. 2.127.0 types the row cells and the meta template (DqTeamwachtrij).",
|
|
4737
|
+
"properties": {
|
|
4738
|
+
"subtitleTemplate": { "type": "string", "minLength": 1, "description": "The meta line: `{field}` tokens filled from the row; empty parts and their separators drop." },
|
|
4739
|
+
"leading": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}, "description": "One cell before the title, drawn as a table column draws it (`{ key: \"createdAt\", widget: \"age\" }`)."},
|
|
4740
|
+
"trailing": { "description": "A cell, or a list of cells, after the badges, each drawn as a table column draws it.", "anyOf": [ {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}}, { "type": "array", "items": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}} } ] }
|
|
4741
|
+
}
|
|
4742
|
+
},
|
|
4721
4743
|
"rowTitle": {
|
|
4722
4744
|
"type": "string",
|
|
4723
4745
|
"enum": ["link", "plain"],
|
|
@@ -64,6 +64,23 @@ export async function fetchGroupMembers(axios, groupId) {
|
|
|
64
64
|
return Array.isArray(users) ? users : Object.values(users)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Every user the reader may list, with their details: all users for an
|
|
69
|
+
* admin, the users of their groups for a sub-admin. Refused for others.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} axios The axios instance.
|
|
72
|
+
* @param {{limit?: number}} [options] How many to read.
|
|
73
|
+
* @return {Promise<Array<object>>} Nextcloud's user details, one per user.
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchUsers(axios, options = {}) {
|
|
76
|
+
const res = await axios.get(generateOcsUrl('cloud/users/details'), {
|
|
77
|
+
params: { limit: options.limit || 500, format: 'json' },
|
|
78
|
+
headers: OCS_HEADERS,
|
|
79
|
+
})
|
|
80
|
+
const users = (ocsData(res) && ocsData(res).users) || {}
|
|
81
|
+
return Array.isArray(users) ? users : Object.values(users)
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Whether a user is in today: an out-of-office period that runs now (the
|
|
69
86
|
* dav app's absence, Nextcloud 28 and later), else their user status.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read an order a manifest writes into the `[{ key, order }]` list a self-fetch
|
|
8
|
+
* index page sorts by. Accepted forms:
|
|
9
|
+
*
|
|
10
|
+
* - `"title"` ascending, `"-createdAt"` descending, `"priority:desc"`;
|
|
11
|
+
* - several of those, comma separated, in priority order (`"-priority,title"`);
|
|
12
|
+
* - an OpenRegister `_order` map, `{ "priority": "desc", "title": "asc" }`;
|
|
13
|
+
* - a `[{ key, order? }]` list.
|
|
14
|
+
*
|
|
15
|
+
* @param {string|object|Array|null|undefined} value The order.
|
|
16
|
+
* @return {Array<{key: string, order: 'asc'|'desc'}>} The sort keys, possibly empty.
|
|
17
|
+
*/
|
|
18
|
+
export function orderToSortKeys(value) {
|
|
19
|
+
const direction = (order) => (String(order || '').toLowerCase() === 'desc' ? 'desc' : 'asc')
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value
|
|
22
|
+
.filter((entry) => entry && typeof entry.key === 'string' && entry.key !== '')
|
|
23
|
+
.map((entry) => ({ key: entry.key, order: direction(entry.order) }))
|
|
24
|
+
}
|
|
25
|
+
if (value && typeof value === 'object') {
|
|
26
|
+
return Object.entries(value)
|
|
27
|
+
.filter(([key]) => key !== '')
|
|
28
|
+
.map(([key, order]) => ({ key, order: direction(order) }))
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== 'string') {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
return value.split(',')
|
|
34
|
+
.map((part) => part.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((part) => {
|
|
37
|
+
if (part.startsWith('-')) {
|
|
38
|
+
return { key: part.slice(1), order: 'desc' }
|
|
39
|
+
}
|
|
40
|
+
const [key, order] = part.split(':')
|
|
41
|
+
return { key, order: direction(order) }
|
|
42
|
+
})
|
|
43
|
+
.filter((entry) => entry.key !== '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether two sort key lists order the same way.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<{key: string, order: string}>} a One list.
|
|
50
|
+
* @param {Array<{key: string, order: string}>} b The other.
|
|
51
|
+
* @return {boolean} True when they name the same keys, directions and priority.
|
|
52
|
+
*/
|
|
53
|
+
export function sameSortKeys(a, b) {
|
|
54
|
+
const left = Array.isArray(a) ? a : []
|
|
55
|
+
const right = Array.isArray(b) ? b : []
|
|
56
|
+
return left.length === right.length
|
|
57
|
+
&& left.every((entry, index) => entry.key === right[index].key && (entry.order || 'asc') === (right[index].order || 'asc'))
|
|
58
|
+
}
|