@conduction/nextcloud-vue 2.75.0 → 2.77.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/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -33
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +242 -19
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -20
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- 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/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectLock.js +41 -10
- package/dist/esm/composables/useObjectLock.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +31 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +376 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +54 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/objectSignals.js +44 -0
- package/dist/esm/utils/objectSignals.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4732 -2990
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +376 -75
- package/l10n/en.json +31 -1
- package/l10n/nl.json +32 -2
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnCredentials/CnCredentials.vue +17 -16
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +80 -20
- package/src/components/CnDetailPage/CnDetailPage.vue +18 -32
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnIndexPage/CnIndexPage.md +51 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +261 -35
- package/src/components/CnIndexPage/selfModeActions.js +89 -13
- package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectLock.js +41 -10
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +35 -1
- package/src/css/table.css +82 -17
- package/src/index.js +3 -1
- package/src/schemas/app-manifest-v2.schema.json +11 -7
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +54 -9
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +2 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/objectSignals.js +41 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/widgetVisibility.js +45 -0
package/src/css/table.css
CHANGED
|
@@ -284,9 +284,64 @@
|
|
|
284
284
|
background-color: var(--color-background-hover);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
/* The sort control: a real button that looks like the header text, the label
|
|
288
|
+
followed by a 14px chevron 4px after it (the "Wettelijke termijn" header on
|
|
289
|
+
the DqWooVerzoeken board). Selector scoped under the table so it outranks
|
|
290
|
+
Nextcloud's own `button` rule, which would otherwise give it a border, a
|
|
291
|
+
fill, a 34px min-height and a margin. */
|
|
292
|
+
.cn-data-table th .cn-table-header__sort {
|
|
293
|
+
display: inline-flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
gap: 4px;
|
|
296
|
+
max-width: 100%;
|
|
297
|
+
min-width: 0;
|
|
298
|
+
min-height: 0;
|
|
299
|
+
margin: 0;
|
|
300
|
+
padding: 0;
|
|
301
|
+
border: 0;
|
|
302
|
+
border-radius: var(--border-radius, 4px);
|
|
303
|
+
background: transparent;
|
|
304
|
+
color: inherit;
|
|
305
|
+
font: inherit;
|
|
306
|
+
font-weight: inherit;
|
|
307
|
+
text-align: start;
|
|
308
|
+
vertical-align: middle;
|
|
309
|
+
cursor: pointer;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.cn-data-table th .cn-table-header__sort:hover,
|
|
313
|
+
.cn-data-table th .cn-table-header__sort:focus {
|
|
314
|
+
background: transparent;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.cn-data-table th .cn-table-header__sort:focus-visible {
|
|
318
|
+
outline: 2px solid var(--color-main-text);
|
|
319
|
+
outline-offset: 2px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cn-data-table th .cn-table-header__sort > :first-child {
|
|
323
|
+
min-width: 0;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
text-overflow: ellipsis;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* The chevron. Unsorted columns show a pale down chevron, so every sortable
|
|
329
|
+
column says it sorts; the sorted column shows the direction at full text
|
|
330
|
+
colour. The direction is also in the header's `aria-sort`. */
|
|
331
|
+
.cn-table-header__chevron {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
flex: none;
|
|
334
|
+
color: var(--color-main-text);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.cn-table-header__chevron--idle {
|
|
338
|
+
color: var(--color-text-maxcontrast);
|
|
339
|
+
opacity: 0.5;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.cn-data-table th.cn-table-header--sortable:hover .cn-table-header__chevron--idle,
|
|
343
|
+
.cn-data-table th .cn-table-header__sort:focus-visible .cn-table-header__chevron--idle {
|
|
344
|
+
opacity: 1;
|
|
290
345
|
}
|
|
291
346
|
|
|
292
347
|
/* A column carrying a `description` gets a dotted underline — the long-standing
|
|
@@ -302,15 +357,12 @@
|
|
|
302
357
|
cursor: help;
|
|
303
358
|
}
|
|
304
359
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
16px icon; the filled icon plus the primary colour marks an active filter,
|
|
312
|
-
so the state never rests on colour alone. */
|
|
313
|
-
.cn-table-header__filter {
|
|
360
|
+
/* Header filter button (CnDataTable `filterable`): a 14px funnel directly
|
|
361
|
+
after the sort chevron, in a 24px hit area. Muted with no filter set; with
|
|
362
|
+
a filter set it takes the dark primary and a dot, so the state never rests
|
|
363
|
+
on colour alone. */
|
|
364
|
+
.cn-data-table th .cn-table-header__filter {
|
|
365
|
+
position: relative;
|
|
314
366
|
display: inline-flex;
|
|
315
367
|
align-items: center;
|
|
316
368
|
justify-content: center;
|
|
@@ -318,7 +370,7 @@
|
|
|
318
370
|
min-width: 24px;
|
|
319
371
|
height: 24px;
|
|
320
372
|
min-height: 24px;
|
|
321
|
-
margin: 0 0 0
|
|
373
|
+
margin: 0 0 0 2px;
|
|
322
374
|
padding: 0;
|
|
323
375
|
border: none;
|
|
324
376
|
border-radius: var(--border-radius, 4px);
|
|
@@ -328,18 +380,31 @@
|
|
|
328
380
|
cursor: pointer;
|
|
329
381
|
}
|
|
330
382
|
|
|
331
|
-
.cn-table-header__filter:hover {
|
|
383
|
+
.cn-data-table th .cn-table-header__filter:hover {
|
|
332
384
|
background-color: var(--color-background-hover);
|
|
333
385
|
color: var(--color-main-text);
|
|
334
386
|
}
|
|
335
387
|
|
|
336
|
-
.cn-table-header__filter:focus-visible {
|
|
388
|
+
.cn-data-table th .cn-table-header__filter:focus-visible {
|
|
337
389
|
outline: 2px solid var(--color-main-text);
|
|
338
390
|
outline-offset: 0;
|
|
339
391
|
}
|
|
340
392
|
|
|
341
|
-
.cn-table-header__filter--active
|
|
342
|
-
|
|
393
|
+
.cn-data-table th .cn-table-header__filter--active,
|
|
394
|
+
.cn-data-table th .cn-table-header__filter--active:hover {
|
|
395
|
+
color: var(--color-primary-element-light-text, var(--color-primary-element));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* The active-filter dot, on the funnel's top right. */
|
|
399
|
+
.cn-table-header__filter-dot {
|
|
400
|
+
position: absolute;
|
|
401
|
+
inset-block-start: 3px;
|
|
402
|
+
inset-inline-end: 3px;
|
|
403
|
+
width: 6px;
|
|
404
|
+
height: 6px;
|
|
405
|
+
border-radius: 50%;
|
|
406
|
+
background-color: var(--color-primary-element);
|
|
407
|
+
pointer-events: none;
|
|
343
408
|
}
|
|
344
409
|
|
|
345
410
|
/* Active header filters above the table, one removable chip each. */
|
package/src/index.js
CHANGED
|
@@ -542,6 +542,8 @@ export { cnFetch, cnFetchJson, CnHttpError, configureCnFetch } from './utils/cnF
|
|
|
542
542
|
// CnLockIndicator, CnObjectCard and CnDataTable, and available to apps that
|
|
543
543
|
// need the same answer without rendering a padlock.
|
|
544
544
|
export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './utils/objectLock.js'
|
|
545
|
+
export { dispatchObjectsChanged } from './utils/objectSignals.js'
|
|
546
|
+
export { lensUnavailableText, readLensReports } from './utils/lensAvailability.js'
|
|
545
547
|
export { columnsFromSchema, fieldsFromSchema, filtersFromSchema, findRepeatingWrite, formatValue, journeyItemTargets, validateValue } from './utils/index.js'
|
|
546
548
|
export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData, usesArrayValues, valueArrayFor, valueRecordsFor } from './utils/index.js'
|
|
547
549
|
// The OpenRegister schema API contract — shared so Buildiq and OpenRegister cannot
|
|
@@ -568,7 +570,7 @@ export {
|
|
|
568
570
|
SENTINEL_RESOLVERS,
|
|
569
571
|
warnIfDeprecated,
|
|
570
572
|
} from './utils/resolveManifestTokens.js'
|
|
571
|
-
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, resetVisibilityCache } from './utils/index.js'
|
|
573
|
+
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, peekCurrentUserGroups, resetVisibilityCache } from './utils/index.js'
|
|
572
574
|
export { safeHref, safeImageSrc, safeSvgPath } from './utils/index.js'
|
|
573
575
|
export { followLinkClick, isModifiedClick, isNewTabHandled, isRowMiddleClick, openRowTarget, resolveHref } from './utils/index.js'
|
|
574
576
|
export { resolveImageUrl } from './utils/index.js'
|
|
@@ -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.75.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -760,8 +760,8 @@
|
|
|
760
760
|
},
|
|
761
761
|
"sentinelFilterToken": {
|
|
762
762
|
"type": "string",
|
|
763
|
-
"pattern": "^@(?:me(?:\\.(?:displayName|email))?|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
|
|
764
|
-
"description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @now, @today, @today±Nd, @monthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
|
|
763
|
+
"pattern": "^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
|
|
764
|
+
"description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @myGroups (the current user's group ids, an IN list), @now, @today, @today±Nd, @monthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
|
|
765
765
|
},
|
|
766
766
|
"sentinelConfigToken": {
|
|
767
767
|
"type": "string",
|
|
@@ -3304,19 +3304,23 @@
|
|
|
3304
3304
|
"properties": {
|
|
3305
3305
|
"favourite": {
|
|
3306
3306
|
"type": "boolean",
|
|
3307
|
-
"description": "
|
|
3307
|
+
"description": "Deprecated and ignored: there is no star any more, a favourite is a follow with notifications off. Kept so existing manifests stay valid; use follow."
|
|
3308
3308
|
},
|
|
3309
3309
|
"follow": {
|
|
3310
3310
|
"type": "boolean",
|
|
3311
|
-
"description": "Detail pages: the Follow
|
|
3311
|
+
"description": "Detail pages: the one Follow control beside the title, with a notifications on/off switch while the user follows and the follower count for a user who may update the record. Omitted means automatic: it shows when the object carries @self.watching. false removes it, and the object read then no longer asks for @self.can."
|
|
3312
|
+
},
|
|
3313
|
+
"showFollowColumn": {
|
|
3314
|
+
"type": "boolean",
|
|
3315
|
+
"description": "Index pages: add a first column with a follow toggle per row, bound to each row's @self.watching. Clicking it does not open the row. Default false."
|
|
3312
3316
|
},
|
|
3313
3317
|
"showFavouriteColumn": {
|
|
3314
3318
|
"type": "boolean",
|
|
3315
|
-
"description": "
|
|
3319
|
+
"description": "Deprecated alias of showFollowColumn: the star column became the follow column. Default false."
|
|
3316
3320
|
},
|
|
3317
3321
|
"personalLenses": {
|
|
3318
3322
|
"type": "array",
|
|
3319
|
-
"description": "Index pages: personal lenses added to the quick filters, in order.
|
|
3323
|
+
"description": "Index pages: personal lenses added to the quick filters, in order. watching lists the records the user follows (labelled Following), recent the records the user opened last (it owns the order, so column sorting is off), unread the records changed since the user last looked. They combine with every other filter. favourite is a deprecated alias of watching; asking for both gives one tab. Default none.",
|
|
3320
3324
|
"items": { "enum": ["favourite", "recent", "watching", "unread"] },
|
|
3321
3325
|
"uniqueItems": true
|
|
3322
3326
|
},
|
|
@@ -3,7 +3,8 @@ import { listWatchers, setFavourite, setReadState, setWatcher, setWatching } fro
|
|
|
3
3
|
/**
|
|
4
4
|
* Interactions plugin for the object store.
|
|
5
5
|
*
|
|
6
|
-
* Per-user interaction calls on a record:
|
|
6
|
+
* Per-user interaction calls on a record: follow (watch, with its
|
|
7
|
+
* notifications switch), the deprecated favourite (a quiet follow), and the
|
|
7
8
|
* followers list. Each write puts the server's answer into the stored object's
|
|
8
9
|
* `@self`, so a list showing the same object agrees with the toggle.
|
|
9
10
|
*
|
|
@@ -55,7 +56,10 @@ export function interactionsPlugin() {
|
|
|
55
56
|
},
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* Star a record.
|
|
59
|
+
* Star a record: a quiet follow on the server.
|
|
60
|
+
*
|
|
61
|
+
* @deprecated A favourite is a follow with notifications off since
|
|
62
|
+
* OpenRegister's `merge-follow-and-favourites`; use `watch(type, id, { notify: false })`.
|
|
59
63
|
*
|
|
60
64
|
* @param {string} type The registered object type slug
|
|
61
65
|
* @param {string} objectId The object ID
|
|
@@ -65,13 +69,15 @@ export function interactionsPlugin() {
|
|
|
65
69
|
const { register, schema } = this._interactionTarget(type)
|
|
66
70
|
const result = await setFavourite(register, schema, objectId, true)
|
|
67
71
|
if (result.ok) {
|
|
68
|
-
this._patchSelf(type, objectId, { favourite: true })
|
|
72
|
+
this._patchSelf(type, objectId, { favourite: true, watching: true })
|
|
69
73
|
}
|
|
70
74
|
return result
|
|
71
75
|
},
|
|
72
76
|
|
|
73
77
|
/**
|
|
74
|
-
* Unstar a record.
|
|
78
|
+
* Unstar a record: unfollow it.
|
|
79
|
+
*
|
|
80
|
+
* @deprecated Use `unwatch`.
|
|
75
81
|
*
|
|
76
82
|
* @param {string} type The registered object type slug
|
|
77
83
|
* @param {string} objectId The object ID
|
|
@@ -81,7 +87,7 @@ export function interactionsPlugin() {
|
|
|
81
87
|
const { register, schema } = this._interactionTarget(type)
|
|
82
88
|
const result = await setFavourite(register, schema, objectId, false)
|
|
83
89
|
if (result.ok) {
|
|
84
|
-
this._patchSelf(type, objectId, { favourite: false })
|
|
90
|
+
this._patchSelf(type, objectId, { favourite: false, watching: false, watchNotify: undefined })
|
|
85
91
|
}
|
|
86
92
|
return result
|
|
87
93
|
},
|
|
@@ -119,17 +125,21 @@ export function interactionsPlugin() {
|
|
|
119
125
|
},
|
|
120
126
|
|
|
121
127
|
/**
|
|
122
|
-
* Follow a record.
|
|
128
|
+
* Follow a record, optionally setting its notifications switch.
|
|
123
129
|
*
|
|
124
130
|
* @param {string} type The registered object type slug
|
|
125
131
|
* @param {string} objectId The object ID
|
|
132
|
+
* @param {object} [options] Options
|
|
133
|
+
* @param {boolean} [options.notify] The notifications switch; left out, a new follow notifies
|
|
126
134
|
* @return {Promise<object>} The outcome
|
|
135
|
+
* @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-one-follow-control-with-a-notifications-switch
|
|
127
136
|
*/
|
|
128
|
-
async watch(type, objectId) {
|
|
137
|
+
async watch(type, objectId, options = {}) {
|
|
129
138
|
const { register, schema } = this._interactionTarget(type)
|
|
130
|
-
const result = await setWatching(register, schema, objectId, true)
|
|
139
|
+
const result = await setWatching(register, schema, objectId, true, options)
|
|
131
140
|
if (result.ok) {
|
|
132
|
-
|
|
141
|
+
const notify = result.data && typeof result.data.notify === 'boolean' ? result.data.notify : true
|
|
142
|
+
this._patchSelf(type, objectId, { watching: true, favourite: true, watchNotify: notify })
|
|
133
143
|
}
|
|
134
144
|
return result
|
|
135
145
|
},
|
|
@@ -145,7 +155,7 @@ export function interactionsPlugin() {
|
|
|
145
155
|
const { register, schema } = this._interactionTarget(type)
|
|
146
156
|
const result = await setWatching(register, schema, objectId, false)
|
|
147
157
|
if (result.ok) {
|
|
148
|
-
this._patchSelf(type, objectId, { watching: false })
|
|
158
|
+
this._patchSelf(type, objectId, { watching: false, favourite: false, watchNotify: undefined })
|
|
149
159
|
}
|
|
150
160
|
return result
|
|
151
161
|
},
|
|
@@ -6,6 +6,7 @@ import { genericError, networkError, parseResponseError } from '../utils/errors.
|
|
|
6
6
|
import { normalizeFacets } from '../utils/facets.js'
|
|
7
7
|
import { buildHeaders, buildQueryString, capitalize, prefixUrl } from '../utils/headers.js'
|
|
8
8
|
import { extractId } from '../utils/id.js'
|
|
9
|
+
import { readLensReports } from '../utils/lensAvailability.js'
|
|
9
10
|
import { dispatchObjectCreated } from '../utils/walkthroughSignals.js'
|
|
10
11
|
import { mergePluginActions, mergePluginGetters, mergePluginState } from './pluginMerge.js'
|
|
11
12
|
import { liveUpdatesPlugin } from './plugins/liveUpdates.js'
|
|
@@ -86,6 +87,16 @@ function baseState(baseUrl = DEFAULT_BASE_URL) {
|
|
|
86
87
|
* @type {{string: object}}
|
|
87
88
|
*/
|
|
88
89
|
facets: {},
|
|
90
|
+
/**
|
|
91
|
+
* Personal-lens reports of the latest collection response per type:
|
|
92
|
+
* the body's `@self.lenses` (`{ recent: { available, reason } }`),
|
|
93
|
+
* `{}` when it carried none. Written with the rows, so it always
|
|
94
|
+
* describes the page on screen.
|
|
95
|
+
*
|
|
96
|
+
* @type {{string: object}}
|
|
97
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
|
|
98
|
+
*/
|
|
99
|
+
lenses: {},
|
|
89
100
|
/** @type {{baseUrl: string, organisationUuidGetter: (() => string|null)|null, languageGetter: (() => string|null)|null, targetLanguageGetter: (() => string|null)|null}} */
|
|
90
101
|
_options: {
|
|
91
102
|
baseUrl: prefixedBaseUrl,
|
|
@@ -195,6 +206,7 @@ const baseGetters = {
|
|
|
195
206
|
* @return {(type: string) => object}
|
|
196
207
|
*/
|
|
197
208
|
getFacets: (state) => (type) => state.facets[type] || {},
|
|
209
|
+
getLenses: (state) => (type) => state.lenses[type] || {},
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
// ── Base actions ────────────────────────────────────────────────────────
|
|
@@ -240,9 +252,10 @@ const baseActions = {
|
|
|
240
252
|
* @param {object} [slugs] Optional slug hints for live-updates transport
|
|
241
253
|
* @param {string|null} [slugs.registerSlug] Canonical register slug (e.g. 'zaken')
|
|
242
254
|
* @param {string|null} [slugs.schemaSlug] Canonical schema slug (e.g. 'meldingen')
|
|
255
|
+
* @param {string} [slugs.collectionUrl] Endpoint `fetchCollection` lists from instead of `/register/schema`; every other call keeps the pair URL
|
|
243
256
|
*/
|
|
244
257
|
registerObjectType(slug, schemaId, registerId, slugs = {}) {
|
|
245
|
-
const { registerSlug = null, schemaSlug = null } = slugs
|
|
258
|
+
const { registerSlug = null, schemaSlug = null, collectionUrl = null } = slugs
|
|
246
259
|
// Replace entire objects so Vue 2 reactivity detects the change
|
|
247
260
|
// (Vue 2 cannot track new properties added to existing reactive objects)
|
|
248
261
|
this.objectTypeRegistry = {
|
|
@@ -252,6 +265,7 @@ const baseActions = {
|
|
|
252
265
|
register: registerId,
|
|
253
266
|
registerSlug,
|
|
254
267
|
schemaSlug,
|
|
268
|
+
...(collectionUrl ? { collectionUrl } : {}),
|
|
255
269
|
},
|
|
256
270
|
}
|
|
257
271
|
this.collections = { ...this.collections, [slug]: [] }
|
|
@@ -263,6 +277,7 @@ const baseActions = {
|
|
|
263
277
|
this.schemas = { ...this.schemas, [slug]: null }
|
|
264
278
|
this.registers = { ...this.registers, [slug]: null }
|
|
265
279
|
this.facets = { ...this.facets, [slug]: {} }
|
|
280
|
+
this.lenses = { ...this.lenses, [slug]: {} }
|
|
266
281
|
},
|
|
267
282
|
|
|
268
283
|
/**
|
|
@@ -306,6 +321,7 @@ const baseActions = {
|
|
|
306
321
|
this.schemas = omit(this.schemas, slug)
|
|
307
322
|
this.registers = omit(this.registers, slug)
|
|
308
323
|
this.facets = omit(this.facets, slug)
|
|
324
|
+
this.lenses = omit(this.lenses, slug)
|
|
309
325
|
},
|
|
310
326
|
|
|
311
327
|
/**
|
|
@@ -439,6 +455,7 @@ const baseActions = {
|
|
|
439
455
|
this.collections = {}
|
|
440
456
|
this.objects = {}
|
|
441
457
|
this.facets = {}
|
|
458
|
+
this.lenses = {}
|
|
442
459
|
this.pagination = {}
|
|
443
460
|
// Errors + loading flags are per-fetch and don't need a reset.
|
|
444
461
|
},
|
|
@@ -496,6 +513,31 @@ const baseActions = {
|
|
|
496
513
|
return url
|
|
497
514
|
},
|
|
498
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Build the list URL for a type registered with a `collectionUrl`. `_order`
|
|
518
|
+
* goes in the bracket form (`_order[key]=dir`) every PHP endpoint parses.
|
|
519
|
+
*
|
|
520
|
+
* @param {object} config The type config
|
|
521
|
+
* @param {object} [params] Query parameters
|
|
522
|
+
* @return {string} Full URL including query string
|
|
523
|
+
*/
|
|
524
|
+
_buildCollectionUrl(config, params = {}) {
|
|
525
|
+
const { _order: order, ...rest } = params
|
|
526
|
+
const merged = { ...rest }
|
|
527
|
+
if (order && typeof order === 'object') {
|
|
528
|
+
for (const [key, dir] of Object.entries(order)) {
|
|
529
|
+
merged[`_order[${key}]`] = dir
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const lang = this._resolveLanguage()
|
|
533
|
+
if (lang) {
|
|
534
|
+
merged._lang = lang
|
|
535
|
+
}
|
|
536
|
+
const url = prefixUrl(config.collectionUrl)
|
|
537
|
+
const query = buildQueryString(merged)
|
|
538
|
+
return query && url.includes('?') ? url + '&' + query.slice(1) : url + query
|
|
539
|
+
},
|
|
540
|
+
|
|
499
541
|
/**
|
|
500
542
|
* Fetch for a type, retrying ONCE with its `schemaFallback` when the
|
|
501
543
|
* schema path segment answers 404. A type without a fallback (every type
|
|
@@ -685,14 +727,15 @@ const baseActions = {
|
|
|
685
727
|
}
|
|
686
728
|
}
|
|
687
729
|
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
730
|
+
const config = this._getTypeConfig(type)
|
|
731
|
+
const init = { method: 'GET', headers: this._buildHeaders() }
|
|
732
|
+
const response = config.collectionUrl
|
|
733
|
+
? await trackedFetch(this._buildCollectionUrl(config, fetchParams), init)
|
|
734
|
+
: await this._fetchWithSchemaFallback(
|
|
735
|
+
type,
|
|
736
|
+
(schema) => this._buildUrlWithParams(type, fetchParams, null, schema),
|
|
737
|
+
init,
|
|
738
|
+
)
|
|
696
739
|
|
|
697
740
|
if (!response.ok) {
|
|
698
741
|
const failure = await parseResponseError(response, type)
|
|
@@ -709,6 +752,8 @@ const baseActions = {
|
|
|
709
752
|
const results = data.results || data
|
|
710
753
|
|
|
711
754
|
this.collections = { ...this.collections, [type]: results }
|
|
755
|
+
// Why a personal lens came back empty, if it says (openregister#4514).
|
|
756
|
+
this.lenses = { ...this.lenses, [type]: readLensReports(data) }
|
|
712
757
|
this.pagination = {
|
|
713
758
|
...this.pagination,
|
|
714
759
|
[type]: {
|
|
@@ -164,13 +164,15 @@ export function formatDaysUntil(value) {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Elapsed-time formatter (`daysSince` registry key), i18n'd through the
|
|
167
|
-
* library's own translation slug (`nextcloud-vue`)
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
167
|
+
* library's own translation slug (`nextcloud-vue`) in the Nextcloud user
|
|
168
|
+
* language. Whole calendar days, read as a running phrase so it sits after a
|
|
169
|
+
* title or in a meta line: today renders "today", one day back renders
|
|
170
|
+
* "yesterday", further back renders "N days ago". A (nonsensical but
|
|
171
|
+
* harmless) future date renders "tomorrow" or "in N days". Day counts are
|
|
172
|
+
* plural-aware via `translatePlural`.
|
|
171
173
|
*
|
|
172
174
|
* Null-safe per the built-in-formatter contract: `''` for null/empty,
|
|
173
|
-
* `String(value)` for unparseable input
|
|
175
|
+
* `String(value)` for unparseable input, never throws.
|
|
174
176
|
*
|
|
175
177
|
* @param {unknown} value A `Date`, parseable date string, or timestamp.
|
|
176
178
|
* @return {string} The relative-day phrasing (or ''/original on bad input).
|
|
@@ -185,13 +187,19 @@ export function formatDaysSince(value) {
|
|
|
185
187
|
}
|
|
186
188
|
const days = dayDiffFromToday(d)
|
|
187
189
|
if (days === 0) {
|
|
188
|
-
return t('nextcloud-vue', '
|
|
190
|
+
return t('nextcloud-vue', 'today')
|
|
191
|
+
}
|
|
192
|
+
if (days === -1) {
|
|
193
|
+
return t('nextcloud-vue', 'yesterday')
|
|
194
|
+
}
|
|
195
|
+
if (days === 1) {
|
|
196
|
+
return t('nextcloud-vue', 'tomorrow')
|
|
189
197
|
}
|
|
190
198
|
if (days < 0) {
|
|
191
199
|
const ago = Math.abs(days)
|
|
192
200
|
return n('nextcloud-vue', '{count} day ago', '{count} days ago', ago, { count: ago })
|
|
193
201
|
}
|
|
194
|
-
return n('nextcloud-vue', '
|
|
202
|
+
return n('nextcloud-vue', 'in {count} day', 'in {count} days', days, { count: days })
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import axios from '@nextcloud/axios'
|
|
16
16
|
import { generateOcsUrl } from '@nextcloud/router'
|
|
17
|
+
import { reactive } from 'vue'
|
|
17
18
|
|
|
18
19
|
const OCS_HEADERS = {
|
|
19
20
|
'OCS-APIRequest': 'true',
|
|
@@ -117,3 +118,77 @@ export async function resolveNextcloudGroup(gid) {
|
|
|
117
118
|
const results = await searchNextcloudGroups(String(gid))
|
|
118
119
|
return results.find((opt) => opt.id === String(gid)) || fallback
|
|
119
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Display names already looked up, by group id. Reactive, so every cell that
|
|
124
|
+
* shows the same group updates when its name arrives.
|
|
125
|
+
*
|
|
126
|
+
* @type {Record<string, string>}
|
|
127
|
+
*/
|
|
128
|
+
const groupNames = reactive({})
|
|
129
|
+
|
|
130
|
+
/** In-flight lookups by group id, so one id is asked for once. */
|
|
131
|
+
const pendingNames = new Map()
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The cached display name of a group, or `null` when it has not been looked
|
|
135
|
+
* up yet. Reactive: read in a computed or a render, it updates when
|
|
136
|
+
* {@link loadGroupDisplayName} finishes.
|
|
137
|
+
*
|
|
138
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
139
|
+
* @param {string} gid The group id.
|
|
140
|
+
* @return {string|null} The display name, or null when unknown.
|
|
141
|
+
*/
|
|
142
|
+
export function groupDisplayName(gid) {
|
|
143
|
+
if (gid === undefined || gid === null || gid === '') {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
const name = groupNames[String(gid)]
|
|
147
|
+
return typeof name === 'string' ? name : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Look a group's display name up once per page and cache it. Concurrent
|
|
152
|
+
* callers for the same id share one request. A failed or empty lookup caches
|
|
153
|
+
* the id itself, so the cell shows the id and does not ask again.
|
|
154
|
+
*
|
|
155
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
156
|
+
* @param {string} gid The group id.
|
|
157
|
+
* @return {Promise<string>} The display name, or the id.
|
|
158
|
+
*/
|
|
159
|
+
export function loadGroupDisplayName(gid) {
|
|
160
|
+
if (gid === undefined || gid === null || gid === '') {
|
|
161
|
+
return Promise.resolve('')
|
|
162
|
+
}
|
|
163
|
+
const key = String(gid)
|
|
164
|
+
const known = groupDisplayName(key)
|
|
165
|
+
if (known !== null) {
|
|
166
|
+
return Promise.resolve(known)
|
|
167
|
+
}
|
|
168
|
+
if (pendingNames.has(key)) {
|
|
169
|
+
return pendingNames.get(key)
|
|
170
|
+
}
|
|
171
|
+
const request = resolveNextcloudGroup(key)
|
|
172
|
+
.then((option) => (option && option.label) || key)
|
|
173
|
+
.catch(() => key)
|
|
174
|
+
.then((label) => {
|
|
175
|
+
groupNames[key] = label
|
|
176
|
+
pendingNames.delete(key)
|
|
177
|
+
return label
|
|
178
|
+
})
|
|
179
|
+
pendingNames.set(key, request)
|
|
180
|
+
return request
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Forget every cached group name (tests, or after a group was renamed).
|
|
185
|
+
*
|
|
186
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
|
|
187
|
+
* @return {void}
|
|
188
|
+
*/
|
|
189
|
+
export function clearGroupNameCache() {
|
|
190
|
+
pendingNames.clear()
|
|
191
|
+
for (const key of Object.keys(groupNames)) {
|
|
192
|
+
delete groupNames[key]
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { findRepeatingWrite, journeyItemTargets } from './journeyRepeatingWrite.
|
|
|
5
5
|
export { columnsFromSchema, fieldsFromSchema, filtersFromSchema, formatValue, validateValue } from './schema.js'
|
|
6
6
|
export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData, usesArrayValues, valueArrayFor, valueRecordsFor } from './dynamicProperties.js'
|
|
7
7
|
export { deleteSchema, describeSchemaChange, saveSchema, SchemaBreakingChangeError, SchemaHasObjectsError } from './schemaApi.js'
|
|
8
|
-
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, resetVisibilityCache } from './widgetVisibility.js'
|
|
8
|
+
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, peekCurrentUserGroups, resetVisibilityCache } from './widgetVisibility.js'
|
|
9
9
|
export { isAppInstalled } from './appInstalled.js'
|
|
10
10
|
export { getAssistantMark } from './assistantMark.js'
|
|
11
11
|
export { passesContextPredicates } from './visibleIfContext.js'
|
|
@@ -19,3 +19,4 @@ export { validateFieldValue } from './formValidation.js'
|
|
|
19
19
|
export { computeObjectDiff } from './computeObjectDiff.js'
|
|
20
20
|
export { foldAuditTrailEntries } from './auditTrailDiff.js'
|
|
21
21
|
export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './objectLock.js'
|
|
22
|
+
export { dispatchObjectsChanged } from './objectSignals.js'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Why a personal lens (Recent, later Following) came back empty.
|
|
6
|
+
*
|
|
7
|
+
* OpenRegister reports per asked lens, at response level, whether it could
|
|
8
|
+
* answer: `@self.lenses.<lens> = { available, reason }` (openregister#4514,
|
|
9
|
+
* `read-history-on-audit-trail`). `available: false` always comes with an
|
|
10
|
+
* empty page. These helpers read that report and turn it into the short
|
|
11
|
+
* explanation a list shows in place of its generic empty text.
|
|
12
|
+
*
|
|
13
|
+
* The lens name is a key, so a later lens (`watching`) only needs copy.
|
|
14
|
+
*
|
|
15
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
16
|
+
*/
|
|
17
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Built-in explanations per lens and reason. Object-neutral: the library
|
|
21
|
+
* does not know whether the app lists cases, tickets or documents. An app
|
|
22
|
+
* says it in its own words through `lensReasonTexts`.
|
|
23
|
+
*
|
|
24
|
+
* @return {Object<string, Object<string, string>>} Lens name to reason to text.
|
|
25
|
+
*/
|
|
26
|
+
function builtInTexts() {
|
|
27
|
+
return {
|
|
28
|
+
recent: {
|
|
29
|
+
'audit-trail-disabled': t('nextcloud-vue', 'This server does not keep track of what you open.'),
|
|
30
|
+
anonymous: t('nextcloud-vue', 'Log in to see what you opened recently.'),
|
|
31
|
+
'read-history-unavailable': t('nextcloud-vue', 'Your recent items are not available right now.'),
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The lens reports of a list response body: `@self.lenses`, or `{}` when the
|
|
38
|
+
* body carries none (every response before openregister#4514, and every
|
|
39
|
+
* response that did not ask for a lens).
|
|
40
|
+
*
|
|
41
|
+
* @param {object|null|undefined} body A parsed list response.
|
|
42
|
+
* @return {Object<string, {available: boolean, reason: (string|null)}>} The reports, keyed by lens name.
|
|
43
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
|
|
44
|
+
*/
|
|
45
|
+
export function readLensReports(body) {
|
|
46
|
+
const lenses = body && typeof body === 'object' && body['@self'] && typeof body['@self'] === 'object'
|
|
47
|
+
? body['@self'].lenses
|
|
48
|
+
: null
|
|
49
|
+
return lenses && typeof lenses === 'object' && !Array.isArray(lenses) ? lenses : {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The explanation for the first lens that reports `available: false` with a
|
|
54
|
+
* reason there is text for, or `''` when there is none (no report, every lens
|
|
55
|
+
* available, or a reason nobody wrote text for). The caller then shows its
|
|
56
|
+
* own empty text, as before.
|
|
57
|
+
*
|
|
58
|
+
* App text wins over built-in text; within app text, `<lens>.<reason>` wins
|
|
59
|
+
* over `<reason>`.
|
|
60
|
+
*
|
|
61
|
+
* @param {object|null|undefined} reports The lens reports (see `readLensReports`).
|
|
62
|
+
* @param {Object<string, string>|null} [overrides] App text keyed `<lens>.<reason>` or `<reason>`.
|
|
63
|
+
* @return {string} The explanation, or an empty string.
|
|
64
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
65
|
+
*/
|
|
66
|
+
export function lensUnavailableText(reports, overrides = null) {
|
|
67
|
+
if (!reports || typeof reports !== 'object') {
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
const own = overrides && typeof overrides === 'object' ? overrides : {}
|
|
71
|
+
const builtIn = builtInTexts()
|
|
72
|
+
for (const [lens, report] of Object.entries(reports)) {
|
|
73
|
+
if (!report || typeof report !== 'object' || report.available !== false) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
const reason = typeof report.reason === 'string' ? report.reason : ''
|
|
77
|
+
if (reason === '') {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const text = own[`${lens}.${reason}`] || own[reason] || (builtIn[lens] && builtIn[lens][reason]) || ''
|
|
81
|
+
if (text) {
|
|
82
|
+
return text
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ''
|
|
86
|
+
}
|