@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
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
v-if="browserRoot !== null"
|
|
9
9
|
:rootPath="browserRoot"
|
|
10
10
|
:rootLabel="browserRootLabel"
|
|
11
|
+
:uploadButton="uploadButton"
|
|
12
|
+
:dropOverlay="dropOverlay"
|
|
13
|
+
:dropHint="dropHint"
|
|
11
14
|
:rowActions="rowActions"
|
|
12
15
|
:newActions="newActions"
|
|
13
16
|
:linkedItems="linkedItems"
|
|
@@ -307,6 +310,12 @@ export default {
|
|
|
307
310
|
deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
|
|
308
311
|
/** What the files browser's root crumb reads; null shows the folder's own name, as the Files app does. */
|
|
309
312
|
browserRootLabel: { type: String, default: null },
|
|
313
|
+
/** Forwarded to CnFilesBrowser: a primary "Add files" button beside the New menu. */
|
|
314
|
+
uploadButton: { type: Boolean, default: false },
|
|
315
|
+
/** Forwarded to CnFilesBrowser: the drop state drawn over the list ("Drop to add") and announced. */
|
|
316
|
+
dropOverlay: { type: Boolean, default: false },
|
|
317
|
+
/** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
|
|
318
|
+
dropHint: { type: Boolean, default: false },
|
|
310
319
|
/**
|
|
311
320
|
* The host's own actions on each file row of the browser, forwarded
|
|
312
321
|
* to CnFilesBrowser's `rowActions`: declared like any manifest action
|
|
@@ -91,6 +91,20 @@
|
|
|
91
91
|
</ul>
|
|
92
92
|
</section>
|
|
93
93
|
|
|
94
|
+
<p v-if="visibleGroups.length && pendingSections.length"
|
|
95
|
+
class="cn-related-objects-widget__section-status"
|
|
96
|
+
data-testid="cn-related-pending"
|
|
97
|
+
role="status">
|
|
98
|
+
{{ pendingLabel }}
|
|
99
|
+
</p>
|
|
100
|
+
<p v-for="failed in failedSections"
|
|
101
|
+
:key="`failed-${failed.key}`"
|
|
102
|
+
class="cn-related-objects-widget__section-error"
|
|
103
|
+
data-testid="cn-related-section-error"
|
|
104
|
+
:data-section="failed.key">
|
|
105
|
+
{{ t('nextcloud-vue', 'Could not load {section}.', { section: failed.label }) }}
|
|
106
|
+
</p>
|
|
107
|
+
|
|
94
108
|
<div v-if="loading && !visibleGroups.length" class="cn-related-objects-widget__empty">
|
|
95
109
|
{{ loadingLabel }}
|
|
96
110
|
</div>
|
|
@@ -710,6 +724,12 @@ export default {
|
|
|
710
724
|
groups: [],
|
|
711
725
|
/** Active tab key. */
|
|
712
726
|
activeKey: '',
|
|
727
|
+
/** Keys of the tabbed sections whose request is still in flight (`objects`, `files`, `relations`). */
|
|
728
|
+
pendingSections: [],
|
|
729
|
+
/** Tabbed sections whose request failed: `{ key, label }`. */
|
|
730
|
+
failedSections: [],
|
|
731
|
+
/** Bumped per `loadTabs()`; an answer from an older load is dropped. */
|
|
732
|
+
loadToken: 0,
|
|
713
733
|
/** Key of the inline-expanded row (`{group}-{id}`) for leaves with no owning-app page (e.g. notes). */
|
|
714
734
|
expandedKey: '',
|
|
715
735
|
/** Whether a footer file upload is in flight. */
|
|
@@ -834,6 +854,22 @@ export default {
|
|
|
834
854
|
)
|
|
835
855
|
},
|
|
836
856
|
|
|
857
|
+
/**
|
|
858
|
+
* The line under the panel naming the sections still loading, once
|
|
859
|
+
* another section is already on screen.
|
|
860
|
+
*
|
|
861
|
+
* @return {string}
|
|
862
|
+
*/
|
|
863
|
+
pendingLabel() {
|
|
864
|
+
const labels = {
|
|
865
|
+
objects: this.objectsLabel,
|
|
866
|
+
files: this.filesLabel,
|
|
867
|
+
relations: t('nextcloud-vue', 'Linked items'),
|
|
868
|
+
}
|
|
869
|
+
const names = this.pendingSections.map((key) => labels[key] || key).join(', ')
|
|
870
|
+
return t('nextcloud-vue', 'Still loading: {sections}', { sections: names })
|
|
871
|
+
},
|
|
872
|
+
|
|
837
873
|
/** Placeholder shown in the body while the first fetch is in flight. */
|
|
838
874
|
loadingLabel() {
|
|
839
875
|
return t('nextcloud-vue', 'Loading …')
|
|
@@ -1330,16 +1366,27 @@ export default {
|
|
|
1330
1366
|
* @return {Promise<object|null>}
|
|
1331
1367
|
*/
|
|
1332
1368
|
async fetchSubResource(suffix) {
|
|
1369
|
+
return (await this.fetchSection(suffix)).data
|
|
1370
|
+
},
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* GET an OpenRegister sub-resource and say whether it answered, so a
|
|
1374
|
+
* section can tell "nothing linked" from "could not load".
|
|
1375
|
+
*
|
|
1376
|
+
* @param {string} suffix - The sub-resource suffix.
|
|
1377
|
+
* @return {Promise<{ ok: boolean, data: object|null }>}
|
|
1378
|
+
*/
|
|
1379
|
+
async fetchSection(suffix) {
|
|
1333
1380
|
try {
|
|
1334
1381
|
// `no-store`: relations change as the user links content; a stale
|
|
1335
1382
|
// cached empty response would wrongly show the empty state on load.
|
|
1336
1383
|
const response = await fetch(this.relatedUrl(suffix), { method: 'GET', headers: buildHeaders(), cache: 'no-store' })
|
|
1337
1384
|
if (!response.ok) {
|
|
1338
|
-
return null
|
|
1385
|
+
return { ok: false, data: null }
|
|
1339
1386
|
}
|
|
1340
|
-
return await response.json()
|
|
1387
|
+
return { ok: true, data: await response.json() }
|
|
1341
1388
|
} catch {
|
|
1342
|
-
return null
|
|
1389
|
+
return { ok: false, data: null }
|
|
1343
1390
|
}
|
|
1344
1391
|
},
|
|
1345
1392
|
|
|
@@ -1543,56 +1590,138 @@ export default {
|
|
|
1543
1590
|
},
|
|
1544
1591
|
|
|
1545
1592
|
/**
|
|
1546
|
-
* Fetch every group directly from OpenRegister and build the tabs
|
|
1593
|
+
* Fetch every group directly from OpenRegister and build the tabs,
|
|
1594
|
+
* one section at a time.
|
|
1595
|
+
*
|
|
1596
|
+
* 🔴 EACH SECTION SHOWS WHEN ITS OWN REQUEST RETURNS. This used to wait
|
|
1597
|
+
* for relations, uses, used, contracts and files together behind one
|
|
1598
|
+
* `Promise.all`, so the relations call (0.45 s on pipelinq's lead page)
|
|
1599
|
+
* sat behind uses/used (about 9 s) and the card said "Loading …" the
|
|
1600
|
+
* whole time. Now the groups are laid out up front in their fixed order
|
|
1601
|
+
* and filled in place as each answer arrives, so a slow section never
|
|
1602
|
+
* reorders or hides a fast one. A section still in flight is named in
|
|
1603
|
+
* `pendingSections`; one whose request failed in `failedSections`.
|
|
1547
1604
|
*
|
|
1605
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/related-objects-progressive/spec.md
|
|
1548
1606
|
* @return {Promise<void>}
|
|
1549
1607
|
*/
|
|
1550
1608
|
async loadTabs() {
|
|
1609
|
+
const token = ++this.loadToken
|
|
1610
|
+
const current = () => token === this.loadToken
|
|
1551
1611
|
this.loading = true
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
])
|
|
1612
|
+
this.failedSections = []
|
|
1613
|
+
|
|
1614
|
+
const objectSuffixes = this.showObjects
|
|
1615
|
+
? ['uses', 'used', ...(this.showContracts ? ['contracts'] : [])]
|
|
1616
|
+
: []
|
|
1617
|
+
const sections = [
|
|
1618
|
+
...(this.showObjects ? [{ key: 'objects', label: this.objectsLabel }] : []),
|
|
1619
|
+
...(this.showFiles ? [{ key: 'files', label: this.filesLabel }] : []),
|
|
1620
|
+
{ key: 'relations', label: t('nextcloud-vue', 'Linked items') },
|
|
1621
|
+
]
|
|
1622
|
+
this.pendingSections = sections.map((section) => section.key)
|
|
1560
1623
|
|
|
1561
|
-
|
|
1624
|
+
// Lay out every group now, in its fixed order, empty. visibleGroups
|
|
1625
|
+
// hides an empty group, so nothing shows until a section fills it.
|
|
1626
|
+
const groups = []
|
|
1627
|
+
if (this.showObjects) {
|
|
1628
|
+
groups.push({ key: 'objects', label: this.objectsLabel, icon: 'FileTreeOutline', integrationId: '', items: [], total: 0 })
|
|
1629
|
+
}
|
|
1630
|
+
if (this.showFiles) {
|
|
1631
|
+
groups.push({ key: 'files', label: this.filesLabel, icon: 'Paperclip', integrationId: 'files', items: [], total: 0 })
|
|
1632
|
+
}
|
|
1633
|
+
for (const def of LEAF_GROUPS) {
|
|
1634
|
+
groups.push({
|
|
1635
|
+
key: def.key,
|
|
1636
|
+
label: this.leafLabel(def.key),
|
|
1637
|
+
// Prefer the registered integration's icon so it matches the
|
|
1638
|
+
// sidebar/linked-apps; fall back to the known-good default.
|
|
1639
|
+
icon: this.integrationIcon(def.integrationId) || def.icon,
|
|
1640
|
+
integrationId: def.integrationId,
|
|
1641
|
+
requiredApp: def.requiredApp,
|
|
1642
|
+
items: [],
|
|
1643
|
+
total: 0,
|
|
1644
|
+
})
|
|
1645
|
+
}
|
|
1646
|
+
this.groups = groups
|
|
1562
1647
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1648
|
+
const setGroup = (key, items, total) => {
|
|
1649
|
+
const index = this.groups.findIndex((group) => group.key === key)
|
|
1650
|
+
if (index === -1) {
|
|
1651
|
+
return
|
|
1567
1652
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1653
|
+
const next = this.groups.slice()
|
|
1654
|
+
next[index] = { ...next[index], items, total }
|
|
1655
|
+
this.groups = next
|
|
1656
|
+
}
|
|
1657
|
+
const settle = (key, ok) => {
|
|
1658
|
+
this.pendingSections = this.pendingSections.filter((k) => k !== key)
|
|
1659
|
+
if (!ok) {
|
|
1660
|
+
const section = sections.find((s) => s.key === key)
|
|
1661
|
+
this.failedSections = [...this.failedSections, { key, label: section ? section.label : key }]
|
|
1572
1662
|
}
|
|
1663
|
+
// Keep the tab the reader saw first: pin it, so a slower section
|
|
1664
|
+
// that sorts before it does not take the panel away.
|
|
1665
|
+
if (!this.activeKey && this.visibleGroups.length) {
|
|
1666
|
+
this.activeKey = this.visibleGroups[0].key
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1573
1669
|
|
|
1670
|
+
const loadRelations = this.fetchSection('relations').then(({ ok, data }) => {
|
|
1671
|
+
if (!current()) {
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1574
1674
|
for (const def of LEAF_GROUPS) {
|
|
1575
|
-
const block =
|
|
1675
|
+
const block = data && data[def.responseKey]
|
|
1576
1676
|
const results = (block && block.results) || []
|
|
1577
|
-
|
|
1578
|
-
key: def.key,
|
|
1579
|
-
label: this.leafLabel(def.key),
|
|
1580
|
-
// Prefer the registered integration's icon so it matches the
|
|
1581
|
-
// sidebar/linked-apps; fall back to the known-good default.
|
|
1582
|
-
icon: this.integrationIcon(def.integrationId) || def.icon,
|
|
1583
|
-
integrationId: def.integrationId,
|
|
1584
|
-
requiredApp: def.requiredApp,
|
|
1585
|
-
items: results.map((r, i) => this.toLeafRow(r, i)),
|
|
1586
|
-
total: block ? (block.total ?? results.length) : 0,
|
|
1587
|
-
})
|
|
1677
|
+
setGroup(def.key, results.map((r, i) => this.toLeafRow(r, i)), block ? (block.total ?? results.length) : 0)
|
|
1588
1678
|
}
|
|
1679
|
+
settle('relations', ok)
|
|
1680
|
+
})
|
|
1589
1681
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1682
|
+
const loadFiles = this.showFiles
|
|
1683
|
+
? this.fetchSection('files').then(({ ok, data }) => {
|
|
1684
|
+
if (!current()) {
|
|
1685
|
+
return
|
|
1686
|
+
}
|
|
1687
|
+
const fileResults = (data && data.results) || []
|
|
1688
|
+
setGroup('files', fileResults.map((f) => this.toFileRow(f)), data ? (data.total ?? fileResults.length) : 0)
|
|
1689
|
+
settle('files', ok)
|
|
1690
|
+
})
|
|
1691
|
+
: null
|
|
1692
|
+
|
|
1693
|
+
// uses, used and contracts merge into one Objects group: each answer
|
|
1694
|
+
// re-merges what has arrived so far, and the group stays pending
|
|
1695
|
+
// until the last of them is in.
|
|
1696
|
+
const envelopes = objectSuffixes.map(() => null)
|
|
1697
|
+
let objectsOk = true
|
|
1698
|
+
const loadObjects = objectSuffixes.length
|
|
1699
|
+
? Promise.all(objectSuffixes.map((suffix, i) => this.fetchSection(suffix).then(async ({ ok, data }) => {
|
|
1700
|
+
if (!current()) {
|
|
1701
|
+
return
|
|
1702
|
+
}
|
|
1703
|
+
objectsOk = objectsOk && ok
|
|
1704
|
+
envelopes[i] = data
|
|
1705
|
+
await this.resolveSchemaTitles([data])
|
|
1706
|
+
if (!current()) {
|
|
1707
|
+
return
|
|
1708
|
+
}
|
|
1709
|
+
const objectItems = this.mergeObjectResults(envelopes)
|
|
1710
|
+
setGroup('objects', objectItems, objectItems.length)
|
|
1711
|
+
}))).then(() => {
|
|
1712
|
+
if (current()) {
|
|
1713
|
+
settle('objects', objectsOk)
|
|
1714
|
+
}
|
|
1715
|
+
})
|
|
1716
|
+
: null
|
|
1717
|
+
|
|
1718
|
+
try {
|
|
1719
|
+
await Promise.all([loadRelations, loadFiles, loadObjects])
|
|
1594
1720
|
} finally {
|
|
1595
|
-
|
|
1721
|
+
if (current()) {
|
|
1722
|
+
this.loading = false
|
|
1723
|
+
this.pendingSections = []
|
|
1724
|
+
}
|
|
1596
1725
|
}
|
|
1597
1726
|
},
|
|
1598
1727
|
|
|
@@ -1896,6 +2025,20 @@ export default {
|
|
|
1896
2025
|
font-style: italic;
|
|
1897
2026
|
}
|
|
1898
2027
|
|
|
2028
|
+
.cn-related-objects-widget__section-status {
|
|
2029
|
+
margin: 0;
|
|
2030
|
+
padding: var(--default-grid-baseline, 4px) calc(2 * var(--default-grid-baseline, 4px));
|
|
2031
|
+
color: var(--color-text-maxcontrast);
|
|
2032
|
+
font-size: 0.85em;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
.cn-related-objects-widget__section-error {
|
|
2036
|
+
margin: 0;
|
|
2037
|
+
padding: var(--default-grid-baseline, 4px) calc(2 * var(--default-grid-baseline, 4px));
|
|
2038
|
+
color: var(--color-error-text, var(--color-error));
|
|
2039
|
+
font-size: 0.85em;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1899
2042
|
.cn-related-objects-widget__empty-state {
|
|
1900
2043
|
/* NcEmptyContent defaults to filling tall parents; inside a widget cell a
|
|
1901
2044
|
compact block reads better. */
|
|
@@ -370,6 +370,19 @@ export default {
|
|
|
370
370
|
default: undefined,
|
|
371
371
|
},
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* App wording for an unavailable personal lens (a `source.filter._recent`
|
|
375
|
+
* tile on an instance that does not log reads), keyed `<lens>.<reason>`
|
|
376
|
+
* or `<reason>`. Forwarded to CnDataTable, which shows the library's
|
|
377
|
+
* object-neutral text without it.
|
|
378
|
+
*
|
|
379
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
380
|
+
*/
|
|
381
|
+
lensReasonTexts: {
|
|
382
|
+
type: Object,
|
|
383
|
+
default: undefined,
|
|
384
|
+
},
|
|
385
|
+
|
|
373
386
|
/**
|
|
374
387
|
* Leading per-row icon: a static MDI icon name string, or a function
|
|
375
388
|
* `(row) => iconName`. Forwarded to CnDataTable.
|
|
@@ -174,3 +174,86 @@ describe('useObjectLock reaches the routes OpenRegister actually declares', () =
|
|
|
174
174
|
})
|
|
175
175
|
})
|
|
176
176
|
})
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run the composable with getter inputs, the way CnDetailPage calls it.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} store The object store double.
|
|
182
|
+
* @param {object} targets Getters for register, schema (cache key), id and slug.
|
|
183
|
+
* @param {Function} body What to do with the lock state.
|
|
184
|
+
* @return {Promise<*>} Whatever `body` returned.
|
|
185
|
+
*/
|
|
186
|
+
async function withGetterLock(store, targets, body) {
|
|
187
|
+
const scope = effectScope()
|
|
188
|
+
let lock
|
|
189
|
+
scope.run(() => {
|
|
190
|
+
lock = useObjectLock(store, targets.register, targets.schema, targets.id, {
|
|
191
|
+
autoRenew: false,
|
|
192
|
+
schemaSlug: targets.slug,
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
try {
|
|
196
|
+
return await body(lock)
|
|
197
|
+
} finally {
|
|
198
|
+
scope.stop()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
describe('useObjectLock reads getter inputs as values', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
mockAxios.post.mockReset().mockResolvedValue({ data: {} })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('🔴 puts the getter VALUES in the lock URL, not their source', async () => {
|
|
208
|
+
await withGetterLock(storeWithLock(null), {
|
|
209
|
+
register: () => 'dossiq',
|
|
210
|
+
schema: () => 'dossiq-case',
|
|
211
|
+
id: () => 'case-1',
|
|
212
|
+
slug: () => 'case',
|
|
213
|
+
}, async (lock) => {
|
|
214
|
+
await lock.acquire()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const [url] = mockAxios.post.mock.calls[0]
|
|
218
|
+
expect(url).toBe('/apps/openregister/api/objects/dossiq/case/case-1/lock')
|
|
219
|
+
expect(url).not.toMatch(/=>/)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('🔴 reads the cached lock under the getter value of the cache key', async () => {
|
|
223
|
+
await withGetterLock(storeWithLock('bram'), {
|
|
224
|
+
register: () => 'dossiq',
|
|
225
|
+
schema: () => 'dossiq-case',
|
|
226
|
+
id: () => 'case-1',
|
|
227
|
+
slug: () => 'case',
|
|
228
|
+
}, async (lock) => {
|
|
229
|
+
expect(lock.locked.value).toBe(true)
|
|
230
|
+
expect(lock.lockedBy.value).toBe('bram')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('🔴 releases on the getter values too', async () => {
|
|
235
|
+
await withGetterLock(storeWithLock('anna'), {
|
|
236
|
+
register: () => 'dossiq',
|
|
237
|
+
schema: () => 'dossiq-case',
|
|
238
|
+
id: () => 'case-1',
|
|
239
|
+
slug: () => 'case',
|
|
240
|
+
}, async (lock) => {
|
|
241
|
+
await lock.release()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(mockAxios.post).toHaveBeenCalledWith('/apps/openregister/api/objects/dossiq/case/case-1/unlock')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it.each([
|
|
248
|
+
['register', { register: () => '', schema: () => 'dossiq-case', id: () => 'case-1', slug: () => 'case' }],
|
|
249
|
+
['schema slug', { register: () => 'dossiq', schema: () => 'dossiq-case', id: () => 'case-1', slug: () => undefined }],
|
|
250
|
+
['object id', { register: () => 'dossiq', schema: () => 'dossiq-case', id: () => null, slug: () => 'case' }],
|
|
251
|
+
])('sends nothing while the %s is unknown', async (_label, targets) => {
|
|
252
|
+
await withGetterLock(storeWithLock('anna'), targets, async (lock) => {
|
|
253
|
+
await lock.acquire()
|
|
254
|
+
await lock.release()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(mockAxios.post).not.toHaveBeenCalled()
|
|
258
|
+
})
|
|
259
|
+
})
|
|
@@ -98,6 +98,9 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
98
98
|
// field (a folder pane grouping by it, say) reads them here rather than
|
|
99
99
|
// deriving them from `objects`, which is only the current page.
|
|
100
100
|
const facets = computed(() => objectStore.facets[getType()] || {})
|
|
101
|
+
// Personal-lens reports of the latest response (`{ recent: { available, reason } }`).
|
|
102
|
+
// @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
|
|
103
|
+
const lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {})
|
|
101
104
|
// Outcome of the latest `refresh()`: the store's error for it, or null once
|
|
102
105
|
// it succeeded or a later fetch stored new rows. The store keeps the previous
|
|
103
106
|
// rows on a failed fetch, so this is how a consumer tells stale rows from
|
|
@@ -381,6 +384,7 @@ export function useListView(objectTypeOrOptions, options) {
|
|
|
381
384
|
loading,
|
|
382
385
|
pagination,
|
|
383
386
|
facets,
|
|
387
|
+
lenses,
|
|
384
388
|
error,
|
|
385
389
|
// Local state
|
|
386
390
|
searchTerm,
|
|
@@ -5,7 +5,7 @@ import { getCurrentUser } from '@nextcloud/auth'
|
|
|
5
5
|
import axios from '@nextcloud/axios'
|
|
6
6
|
import { generateUrl } from '@nextcloud/router'
|
|
7
7
|
import { tryOnScopeDispose } from '@vueuse/core'
|
|
8
|
-
import { computed, onBeforeUnmount, onMounted,
|
|
8
|
+
import { computed, onBeforeUnmount, onMounted, toValue } from 'vue'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Thrown when an `acquire()` POST returns 409 (Conflict) or 423
|
|
@@ -46,14 +46,14 @@ export class PermissionError extends Error {
|
|
|
46
46
|
* other path that keeps `objectStore.objects[type][id]` fresh).
|
|
47
47
|
*
|
|
48
48
|
* @param {object} objectStore Pinia store instance (`useObjectStore()` result).
|
|
49
|
-
* @param {string|import('vue').Ref<string
|
|
50
|
-
* @param {string|import('vue').Ref<string
|
|
51
|
-
* @param {string|import('vue').Ref<string
|
|
49
|
+
* @param {string|import('vue').Ref<string>|function(): string} register OR register slug.
|
|
50
|
+
* @param {string|import('vue').Ref<string>|function(): string} schema OR schema slug (also the GraphQL field name).
|
|
51
|
+
* @param {string|import('vue').Ref<string>|function(): string} id Object UUID.
|
|
52
52
|
* @param {object} [options] Optional config.
|
|
53
53
|
* @param {boolean} [options.autoRenew] Renew the lock on a fixed interval while editing + visible.
|
|
54
54
|
* @param {number} [options.renewIntervalMs] Renew every N ms (default 10 min).
|
|
55
55
|
* @param {number} [options.lockDurationSec] Server-side TTL requested on acquire (default 30 min).
|
|
56
|
-
* @param {string|import('vue').Ref<string
|
|
56
|
+
* @param {string|import('vue').Ref<string>|function(): string} [options.schemaSlug] The schema slug for the LOCK URL, when `schema` is the object-cache key rather than the slug (CnDetailPage passes `<register>-<schema>` as the key). Defaults to `schema`.
|
|
57
57
|
* @return {{
|
|
58
58
|
* locked: import('vue').ComputedRef<boolean>,
|
|
59
59
|
* lockedByMe: import('vue').ComputedRef<boolean>,
|
|
@@ -62,6 +62,7 @@ export class PermissionError extends Error {
|
|
|
62
62
|
* acquire: () => Promise<void>,
|
|
63
63
|
* release: () => Promise<void>
|
|
64
64
|
* }} Reactive lock state + actions.
|
|
65
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md
|
|
65
66
|
*/
|
|
66
67
|
export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
67
68
|
const autoRenew = options.autoRenew !== false
|
|
@@ -74,14 +75,23 @@ export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
74
75
|
const isVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
75
76
|
let renewTimer = null
|
|
76
77
|
|
|
78
|
+
/*
|
|
79
|
+
* 🔴 `toValue`, NOT `unref`. CnDetailPage passes GETTER FUNCTIONS
|
|
80
|
+
* (`() => props.objectId`) so the lock follows the route. `unref` returns a
|
|
81
|
+
* function untouched, so the getter's SOURCE was interpolated into the URL:
|
|
82
|
+
* every detail page sent `/api/objects/()=>pn/()=>Ct.schema||Bn/()=>Ct.objectId/lock`
|
|
83
|
+
* and got a 404, and the cache lookup used the function as its key, so the
|
|
84
|
+
* banner never saw a lock either. `toValue` reads a plain value, a ref and a
|
|
85
|
+
* getter alike.
|
|
86
|
+
*/
|
|
77
87
|
function readType() {
|
|
78
|
-
return
|
|
88
|
+
return toValue(schema)
|
|
79
89
|
}
|
|
80
90
|
function readId() {
|
|
81
|
-
return
|
|
91
|
+
return toValue(id)
|
|
82
92
|
}
|
|
83
93
|
function readRegister() {
|
|
84
|
-
return
|
|
94
|
+
return toValue(register)
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
function readSelfLock() {
|
|
@@ -127,7 +137,22 @@ export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
127
137
|
})
|
|
128
138
|
|
|
129
139
|
function readSchemaSlug() {
|
|
130
|
-
return
|
|
140
|
+
return toValue(options.schemaSlug ?? schema)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether register, schema slug and object id are all known.
|
|
145
|
+
*
|
|
146
|
+
* A detail page mounts before its route params or object have resolved.
|
|
147
|
+
* Without this a lock call would go out with `undefined` in the path, a
|
|
148
|
+
* request OpenRegister can only answer with a 404.
|
|
149
|
+
*
|
|
150
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-lock/spec.md
|
|
151
|
+
* @return {boolean} True when a lock URL can be built.
|
|
152
|
+
*/
|
|
153
|
+
function hasTarget() {
|
|
154
|
+
return [readRegister(), readSchemaSlug(), readId()]
|
|
155
|
+
.every((part) => typeof part === 'string' ? part !== '' : typeof part === 'number')
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
/**
|
|
@@ -176,6 +201,9 @@ export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
176
201
|
}
|
|
177
202
|
|
|
178
203
|
async function acquire() {
|
|
204
|
+
if (!hasTarget()) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
179
207
|
try {
|
|
180
208
|
await axios.post(endpoint(), { duration: lockDurationSec })
|
|
181
209
|
} catch (e) {
|
|
@@ -208,6 +236,9 @@ export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
208
236
|
|
|
209
237
|
async function release() {
|
|
210
238
|
stopRenewTimer()
|
|
239
|
+
if (!hasTarget()) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
211
242
|
try {
|
|
212
243
|
await axios.post(unlockEndpoint())
|
|
213
244
|
} catch (e) {
|
|
@@ -251,7 +282,7 @@ export function useObjectLock(objectStore, register, schema, id, options = {}) {
|
|
|
251
282
|
}
|
|
252
283
|
|
|
253
284
|
function beaconRelease() {
|
|
254
|
-
if (!lockedByMe.value) {
|
|
285
|
+
if (!lockedByMe.value || !hasTarget()) {
|
|
255
286
|
return
|
|
256
287
|
}
|
|
257
288
|
try {
|
|
@@ -114,12 +114,26 @@
|
|
|
114
114
|
|
|
115
115
|
/* ---- Folder tabs ------------------------------------------------------- */
|
|
116
116
|
|
|
117
|
+
/* The strip draws no line of its own (design-system pull request 147). The card under it
|
|
118
|
+
carries its full border, top included, so the top-right corner is the
|
|
119
|
+
card's own rounded corner instead of a strip rule running straight past it.
|
|
120
|
+
The open tab overlaps that top border by 1px (the nav's -1px margin) and
|
|
121
|
+
paints over it, so it flows into the card as a folder tab. */
|
|
117
122
|
.cn-look-board .cn-tabs--board .cn-tabs__bar {
|
|
118
|
-
border-bottom:
|
|
123
|
+
border-bottom: none;
|
|
119
124
|
gap: 4px;
|
|
125
|
+
position: relative;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cn-look-board .cn-tabs.cn-tabs--board .cn-tabs__bar::after {
|
|
129
|
+
content: none;
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/* Tabs sit on the card's top edge; only the open tab reaches 1px further
|
|
133
|
+
down (min-height 35px below), over the card's top border. Top-aligned so
|
|
134
|
+
the inactive tabs do not stretch to the open tab's height. */
|
|
122
135
|
.cn-look-board .cn-tabs--board .cn-tabs__nav {
|
|
136
|
+
align-items: flex-start;
|
|
123
137
|
gap: 4px;
|
|
124
138
|
}
|
|
125
139
|
|
|
@@ -140,7 +154,11 @@
|
|
|
140
154
|
border-bottom: 1px solid var(--color-main-background);
|
|
141
155
|
border-top: 1px solid var(--color-border-dark);
|
|
142
156
|
font-weight: 700;
|
|
157
|
+
min-height: 35px;
|
|
158
|
+
padding-bottom: 9px;
|
|
143
159
|
padding-top: 8px;
|
|
160
|
+
position: relative;
|
|
161
|
+
z-index: 1;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
164
|
/* The count: a grey, borderless 20px badge 8px after the label. */
|
|
@@ -177,10 +195,30 @@
|
|
|
177
195
|
.cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > :first-child .cn-widget-wrapper,
|
|
178
196
|
.cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > .cn-widget-wrapper,
|
|
179
197
|
.cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > .cn-detail-card {
|
|
180
|
-
border-top:
|
|
198
|
+
border-top: 1px solid var(--color-border);
|
|
181
199
|
border-radius: 0 12px 12px 12px;
|
|
182
200
|
}
|
|
183
201
|
|
|
202
|
+
/* A CnTabsWidget panel is the card itself: the same full border, so it joins
|
|
203
|
+
the open tab the same way. The top-left stays square under the first tab. */
|
|
204
|
+
.cn-look-board .cn-tabs--board.cn-tabs-widget__tabs .cn-tabs__content {
|
|
205
|
+
border: 1px solid var(--color-border);
|
|
206
|
+
border-radius: 0 12px 12px 12px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* A panel of CnTabsWidget is the content block itself: the open tab flows
|
|
210
|
+
straight into it. The widget in the panel renders chromeless, but the board
|
|
211
|
+
card rule below (and the first-card rule above) used to give its wrapper a
|
|
212
|
+
border and a 12px radius again, so a second card sat inside the panel. Its
|
|
213
|
+
left edge ran down from the open tab and its rounded top-right drew a line
|
|
214
|
+
across the top right of the content block, where DqZaak shows one sheet.
|
|
215
|
+
Same specificity as the first-card rule and later, so it wins on order. */
|
|
216
|
+
.cn-look-board .cn-tabs--board.cn-tabs-widget__tabs .cn-tabs__content .cn-widget-wrapper.cn-widget-wrapper--chromeless {
|
|
217
|
+
background: transparent;
|
|
218
|
+
border: 0;
|
|
219
|
+
border-radius: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
184
222
|
/* ---- Body cards and side cards ----------------------------------------- */
|
|
185
223
|
|
|
186
224
|
.cn-look-board .cn-detail-page .cn-widget-wrapper,
|
|
@@ -343,7 +343,10 @@
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
.cn-look-board .cn-index-page__main.cn-index-page__main--table {
|
|
346
|
-
|
|
346
|
+
/* The body is a row: the card takes the width left beside a sidebar (the
|
|
347
|
+
board's table card spans the content), and keeps the height of its rows. */
|
|
348
|
+
flex: 1 1 auto;
|
|
349
|
+
align-self: flex-start;
|
|
347
350
|
border: 1px solid var(--color-border);
|
|
348
351
|
border-radius: var(--cn-board-card-radius, 12px);
|
|
349
352
|
background: var(--color-main-background);
|
|
@@ -533,6 +536,37 @@
|
|
|
533
536
|
background: var(--color-primary-element-light);
|
|
534
537
|
}
|
|
535
538
|
|
|
539
|
+
/* The status accent outranks the board's border, also on hover and selection. */
|
|
540
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent {
|
|
541
|
+
border-inline-start-width: 4px;
|
|
542
|
+
padding-inline-start: 13px;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-success,
|
|
546
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-success:hover {
|
|
547
|
+
border-inline-start-color: var(--color-element-success);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-warning,
|
|
551
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-warning:hover {
|
|
552
|
+
border-inline-start-color: var(--color-element-warning);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-error,
|
|
556
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-error:hover {
|
|
557
|
+
border-inline-start-color: var(--color-element-error);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-info,
|
|
561
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-info:hover {
|
|
562
|
+
border-inline-start-color: var(--color-element-info);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-primary,
|
|
566
|
+
.cn-look-board .cn-object-card.cn-object-card--board.cn-object-card--accent-primary:hover {
|
|
567
|
+
border-inline-start-color: var(--color-primary-element);
|
|
568
|
+
}
|
|
569
|
+
|
|
536
570
|
.cn-look-board .cn-object-card--board .cn-object-card__content {
|
|
537
571
|
display: flex;
|
|
538
572
|
flex: 1 1 auto;
|