@conduction/nextcloud-vue 2.78.0 → 2.80.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/bundled-packages.json +0 -2
- package/dist/esm/bundled-packages.json +0 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
- 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/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/index.js +30 -0
- package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/index.js +29 -0
- package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/index.js +31 -0
- package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/index.js +35 -0
- package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.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/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useAppManifest.js +8 -3
- package/dist/esm/composables/useAppManifest.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +140 -9
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection.js +21 -0
- package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
- package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
- package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
- package/dist/esm/integrations/offline/index.js +2 -1
- package/dist/esm/integrations/offline/index.js.map +1 -1
- package/dist/esm/integrations/offline/offlineDb.js +6 -1
- package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
- package/dist/esm/integrations/offline/syncReplayService.js +8 -2
- package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
- package/dist/esm/l10n/en.json.js +72 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +120 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1314 -74
- package/dist/esm/utils/boardDate.js +193 -0
- package/dist/esm/utils/boardDate.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +60 -3
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/dynamicProperties.js +5 -1
- package/dist/esm/utils/dynamicProperties.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +114 -0
- package/dist/esm/utils/fillTemplate.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/queryPresets.js +96 -0
- package/dist/esm/utils/queryPresets.js.map +1 -0
- package/dist/esm/utils/resolveFilterTokens.js +11 -0
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +21 -0
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +3 -3
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +7 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14278 -8169
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1314 -74
- package/l10n/en.json +72 -1
- package/l10n/nl.json +120 -2
- package/package.json +1 -1
- package/scripts/build-validators.js +26 -0
- package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
- package/src/components/CnActionsBar/CnActionsBar.md +4 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
- package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
- package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
- package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
- package/src/components/CnDataTable/CnDataTable.md +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +82 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
- package/src/components/CnFactsWidget/index.js +29 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
- package/src/components/CnFieldListWidget/index.js +28 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
- package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
- package/src/components/CnFilesBrowser/index.js +1 -0
- package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
- package/src/components/CnFormDialog/CnFormDialog.md +9 -3
- package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
- package/src/components/CnFormPage/CnFormPage.vue +18 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
- package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
- package/src/components/CnRecordListWidget/index.js +30 -0
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
- package/src/components/CnSectionsWidget/index.js +34 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
- package/src/components/index.js +5 -1
- package/src/composables/__tests__/useFormDraft.spec.js +69 -0
- package/src/composables/useAppManifest.js +8 -3
- package/src/composables/useFormDraft.js +139 -8
- package/src/css/form-field.css +14 -0
- package/src/css/index-page.css +71 -0
- package/src/css/kpi-card.css +32 -0
- package/src/css/look-board-detail.css +87 -0
- package/src/css/look-board-index.css +143 -14
- package/src/css/look-board.css +114 -2
- package/src/css/table.css +56 -0
- package/src/index.js +8 -0
- package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
- package/src/integrations/builtin/field-inspection.js +21 -0
- package/src/integrations/offline/checklistSubmission.js +137 -0
- package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
- package/src/integrations/offline/index.js +3 -0
- package/src/integrations/offline/offlineDb.js +6 -1
- package/src/integrations/offline/syncReplayService.js +8 -2
- package/src/schemas/app-manifest-v2.schema.json +149 -5
- package/src/utils/boardDate.js +189 -0
- package/src/utils/detailActionModel.js +59 -2
- package/src/utils/dynamicProperties.js +5 -1
- package/src/utils/fillTemplate.js +110 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/queryPresets.js +93 -0
- package/src/utils/resolveFilterTokens.js +11 -0
- package/src/utils/schema.js +21 -0
- package/src/utils/sentinelTokens.js +3 -3
- package/src/utils/validateManifestV2.compiled.js +57 -2
- package/src/utils/widgetDispatch.js +7 -1
- package/dist/esm/_virtual/equal.js +0 -4
- package/dist/esm/_virtual/equal.js.map +0 -1
- package/dist/esm/_virtual/ucs2length.js +0 -4
- package/dist/esm/_virtual/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
- package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
|
@@ -17,11 +17,21 @@
|
|
|
17
17
|
* one, and it would be this library's fault in somebody else's app.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
jest.mock('@nextcloud/auth', () => ({
|
|
21
|
+
...jest.requireActual('@nextcloud/auth'),
|
|
22
|
+
getCurrentUser: jest.fn(() => null),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
import { getCurrentUser } from '@nextcloud/auth'
|
|
20
26
|
import {
|
|
27
|
+
clearAnonymousDrafts,
|
|
21
28
|
clearDraft,
|
|
22
29
|
DRAFT_MAX_AGE_MS,
|
|
23
30
|
draftKey,
|
|
31
|
+
hasDraftChanges,
|
|
24
32
|
readDraft,
|
|
33
|
+
resolveDraftUserId,
|
|
34
|
+
sameDraftValue,
|
|
25
35
|
writeDraft,
|
|
26
36
|
} from '../useFormDraft.js'
|
|
27
37
|
|
|
@@ -170,3 +180,62 @@ describe('when storage is not available', () => {
|
|
|
170
180
|
expect(() => clearDraft('cn-form-draft:a:s:new:u')).not.toThrow()
|
|
171
181
|
})
|
|
172
182
|
})
|
|
183
|
+
|
|
184
|
+
// @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
|
|
185
|
+
describe('resolveDraftUserId', () => {
|
|
186
|
+
afterEach(() => getCurrentUser.mockReturnValue(null))
|
|
187
|
+
|
|
188
|
+
it('prefers the id the host passed', () => {
|
|
189
|
+
getCurrentUser.mockReturnValue({ uid: 'cluade' })
|
|
190
|
+
expect(resolveDraftUserId('anne')).toBe('anne')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('falls back to the signed-in user', () => {
|
|
194
|
+
getCurrentUser.mockReturnValue({ uid: 'cluade' })
|
|
195
|
+
expect(resolveDraftUserId('')).toBe('cluade')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('is empty when nobody is signed in, and when the lookup throws', () => {
|
|
199
|
+
expect(resolveDraftUserId()).toBe('')
|
|
200
|
+
getCurrentUser.mockImplementationOnce(() => {
|
|
201
|
+
throw new Error('no head')
|
|
202
|
+
})
|
|
203
|
+
expect(resolveDraftUserId()).toBe('')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('clearAnonymousDrafts', () => {
|
|
208
|
+
beforeEach(() => window.localStorage.clear())
|
|
209
|
+
|
|
210
|
+
it('removes only drafts stored under anonymous', () => {
|
|
211
|
+
const anon = draftKey({ appId: 'pipelinq', schema: 'crmTask', objectId: 't1' })
|
|
212
|
+
const anne = draftKey({ appId: 'pipelinq', schema: 'crmTask', objectId: 't1', userId: 'anne' })
|
|
213
|
+
writeDraft(anon, { title: 'x' })
|
|
214
|
+
writeDraft(anne, { title: 'y' })
|
|
215
|
+
window.localStorage.setItem('unrelated:anonymous', 'keep')
|
|
216
|
+
|
|
217
|
+
expect(clearAnonymousDrafts()).toBe(1)
|
|
218
|
+
expect(window.localStorage.getItem(anon)).toBeNull()
|
|
219
|
+
expect(window.localStorage.getItem(anne)).not.toBeNull()
|
|
220
|
+
expect(window.localStorage.getItem('unrelated:anonymous')).toBe('keep')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
|
|
225
|
+
describe('hasDraftChanges', () => {
|
|
226
|
+
it('treats empty values as equal', () => {
|
|
227
|
+
expect(sameDraftValue(null, '')).toBe(true)
|
|
228
|
+
expect(sameDraftValue(undefined, [])).toBe(true)
|
|
229
|
+
expect(sameDraftValue('a', 'b')).toBe(false)
|
|
230
|
+
expect(sameDraftValue({ a: 1 }, { a: 1 })).toBe(true)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('counts only fields a person edited', () => {
|
|
234
|
+
const baseline = { title: 'Call back', notes: '' }
|
|
235
|
+
// The form changed `notes` by itself; nobody edited it.
|
|
236
|
+
expect(hasDraftChanges({ title: 'Call back', notes: 'filled in' }, baseline, [])).toBe(false)
|
|
237
|
+
// Edited, but back to what it was.
|
|
238
|
+
expect(hasDraftChanges({ title: 'Call back' }, baseline, ['title'])).toBe(false)
|
|
239
|
+
expect(hasDraftChanges({ title: 'Call back tomorrow' }, baseline, ['title'])).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -205,6 +205,7 @@ function loadInMemory(input) {
|
|
|
205
205
|
* @param {object} options Configuration options (endpoint / fetcher /
|
|
206
206
|
* getAppConfigValue overrides).
|
|
207
207
|
* @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
|
|
208
|
+
* @spec openspec/changes/manifest-validator-self-contained/specs/manifest-validator-self-contained/spec.md#requirement-a-backend-manifest-delta-reaches-the-navigation
|
|
208
209
|
*/
|
|
209
210
|
function loadFromBackend(appId, bundledManifest, options) {
|
|
210
211
|
// Shallow by default (manifest-shallow-reactivity-by-default) — see
|
|
@@ -284,9 +285,13 @@ function loadFromBackend(appId, bundledManifest, options) {
|
|
|
284
285
|
// The shallowRef reassignment is what re-renders consumers; the new
|
|
285
286
|
// object stays shallow (not deep-observed) unless CnAppRoot upgrades it.
|
|
286
287
|
manifest.value = resolved
|
|
287
|
-
} catch {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Any unexpected error leaves the bundled manifest in place, so
|
|
290
|
+
// apps keep working. Say so: a silent catch here once hid a
|
|
291
|
+
// validator that threw on every manifest, and the backend delta
|
|
292
|
+
// never reached the navigation.
|
|
293
|
+
// eslint-disable-next-line no-console
|
|
294
|
+
console.warn('[useAppManifest] Could not resolve the manifest; keeping the bundled manifest.', error)
|
|
290
295
|
} finally {
|
|
291
296
|
isLoading.value = false
|
|
292
297
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* value, and the audit trail is read by people answering "who changed this".
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { getCurrentUser } from '@nextcloud/auth'
|
|
26
27
|
import { translate as t } from '@nextcloud/l10n'
|
|
27
28
|
|
|
28
29
|
/** How long a draft stays interesting, in milliseconds (7 days). */
|
|
@@ -34,6 +35,9 @@ export const DRAFT_DEBOUNCE_MS = 500
|
|
|
34
35
|
/** The prefix every draft key carries, so a sweep can find them. */
|
|
35
36
|
export const DRAFT_KEY_PREFIX = 'cn-form-draft'
|
|
36
37
|
|
|
38
|
+
/** The user part of a draft key when nobody is signed in. */
|
|
39
|
+
export const DRAFT_ANONYMOUS_USER = 'anonymous'
|
|
40
|
+
|
|
37
41
|
/**
|
|
38
42
|
* The storage key one form's draft lives under.
|
|
39
43
|
*
|
|
@@ -54,10 +58,124 @@ export function draftKey({ appId = '', schema = '', objectId = '', userId = '' }
|
|
|
54
58
|
String(appId || 'app'),
|
|
55
59
|
String(schema || 'schema'),
|
|
56
60
|
String(objectId || 'new'),
|
|
57
|
-
String(userId ||
|
|
61
|
+
String(userId || DRAFT_ANONYMOUS_USER),
|
|
58
62
|
].join(':')
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Who a draft belongs to: the id the host passed, else the signed-in
|
|
67
|
+
* Nextcloud user, else '' (which {@link draftKey} stores as `anonymous`).
|
|
68
|
+
*
|
|
69
|
+
* 🔴 A HOST THAT PASSES NOTHING STILL GETS PER-USER DRAFTS. Leaving the user
|
|
70
|
+
* to every host meant no host passed it, and every draft on a shared device
|
|
71
|
+
* was keyed `anonymous`: the next person at the counter was offered what the
|
|
72
|
+
* last one typed.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} [explicit] The id the host passed (`draftUserId`).
|
|
75
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
76
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
|
|
77
|
+
*/
|
|
78
|
+
export function resolveDraftUserId(explicit = '') {
|
|
79
|
+
if (explicit) {
|
|
80
|
+
return String(explicit)
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const uid = getCurrentUser()?.uid
|
|
84
|
+
return uid ? String(uid) : ''
|
|
85
|
+
} catch {
|
|
86
|
+
// No Nextcloud page around us (a test, an embed): nobody is signed in.
|
|
87
|
+
return ''
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove every draft stored under the anonymous user.
|
|
93
|
+
*
|
|
94
|
+
* Called once a signed-in user is known. Such a draft was written by a page
|
|
95
|
+
* that did not know who was typing, so it may be somebody else's, and it is
|
|
96
|
+
* deleted rather than moved: moving it would hand it to whoever opened the
|
|
97
|
+
* form next.
|
|
98
|
+
*
|
|
99
|
+
* @return {number} How many drafts were removed.
|
|
100
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
|
|
101
|
+
*/
|
|
102
|
+
export function clearAnonymousDrafts() {
|
|
103
|
+
let removed = 0
|
|
104
|
+
try {
|
|
105
|
+
const storage = window.localStorage
|
|
106
|
+
const suffix = ':' + DRAFT_ANONYMOUS_USER
|
|
107
|
+
const doomed = []
|
|
108
|
+
for (let i = 0; i < storage.length; i++) {
|
|
109
|
+
const key = storage.key(i)
|
|
110
|
+
if (key && key.startsWith(DRAFT_KEY_PREFIX + ':') && key.endsWith(suffix)) {
|
|
111
|
+
doomed.push(key)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const key of doomed) {
|
|
115
|
+
storage.removeItem(key)
|
|
116
|
+
removed++
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Blocked storage holds no drafts to leak.
|
|
120
|
+
}
|
|
121
|
+
return removed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether a value counts as empty for a draft: null, undefined, '' and [].
|
|
126
|
+
*
|
|
127
|
+
* @param {unknown} value The value.
|
|
128
|
+
* @return {boolean} True when empty.
|
|
129
|
+
*/
|
|
130
|
+
function isEmptyDraftValue(value) {
|
|
131
|
+
return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether two field values are the same for the purpose of a draft.
|
|
136
|
+
*
|
|
137
|
+
* Empty values are equal to each other: an input reports a missing value back
|
|
138
|
+
* as '', and that is the control normalising, not a person typing.
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} a One value.
|
|
141
|
+
* @param {unknown} b The other.
|
|
142
|
+
* @return {boolean} True when they are the same.
|
|
143
|
+
*/
|
|
144
|
+
export function sameDraftValue(a, b) {
|
|
145
|
+
if (isEmptyDraftValue(a) && isEmptyDraftValue(b)) {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
if (a === b) {
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
153
|
+
} catch {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether a person changed the form: one of the fields they edited holds a
|
|
160
|
+
* value different from the one the form opened with.
|
|
161
|
+
*
|
|
162
|
+
* 🔴 ONLY EDITED FIELDS COUNT. A form fills in values by itself after it
|
|
163
|
+
* opens (defaults, normalised dates, the active organisation, options that
|
|
164
|
+
* arrive later). Counting those wrote a draft and said "Saved just now" on a
|
|
165
|
+
* dialog nobody had touched.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} values The current values.
|
|
168
|
+
* @param {object} baseline The values the form opened with.
|
|
169
|
+
* @param {string[]} editedKeys The fields a person edited.
|
|
170
|
+
* @return {boolean} True when there is something worth keeping.
|
|
171
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
|
|
172
|
+
*/
|
|
173
|
+
export function hasDraftChanges(values, baseline, editedKeys) {
|
|
174
|
+
const now = values || {}
|
|
175
|
+
const base = baseline || {}
|
|
176
|
+
return (editedKeys || []).some((key) => !sameDraftValue(now[key], base[key]))
|
|
177
|
+
}
|
|
178
|
+
|
|
61
179
|
/**
|
|
62
180
|
* Read one draft back, or null when there is nothing usable.
|
|
63
181
|
*
|
|
@@ -218,13 +336,6 @@ export function formDraftMixin() {
|
|
|
218
336
|
draftSavedAt: 0,
|
|
219
337
|
/** Non-reactive handle for the debounce. */
|
|
220
338
|
draftTimeout: null,
|
|
221
|
-
/**
|
|
222
|
-
* Whether the form's own initial population has happened.
|
|
223
|
-
*
|
|
224
|
-
* The first change to the values is the component building
|
|
225
|
-
* them, not a person typing. See the watcher that reads this.
|
|
226
|
-
*/
|
|
227
|
-
draftSeeded: false,
|
|
228
339
|
}
|
|
229
340
|
},
|
|
230
341
|
|
|
@@ -265,6 +376,26 @@ export function formDraftMixin() {
|
|
|
265
376
|
}, DRAFT_DEBOUNCE_MS)
|
|
266
377
|
},
|
|
267
378
|
|
|
379
|
+
/**
|
|
380
|
+
* The form is back to what it opened with: cancel a pending write,
|
|
381
|
+
* remove a draft written earlier and stop announcing anything.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} key The storage key.
|
|
384
|
+
* @return {void}
|
|
385
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
|
|
386
|
+
*/
|
|
387
|
+
settleUnchangedDraft(key) {
|
|
388
|
+
if (this.draftTimeout) {
|
|
389
|
+
clearTimeout(this.draftTimeout)
|
|
390
|
+
this.draftTimeout = null
|
|
391
|
+
}
|
|
392
|
+
if (this.draftState !== 'idle') {
|
|
393
|
+
clearDraft(key)
|
|
394
|
+
this.draftState = 'idle'
|
|
395
|
+
this.draftSavedAt = 0
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
268
399
|
/**
|
|
269
400
|
* Drop the draft and stop announcing anything about it.
|
|
270
401
|
*
|
package/src/css/form-field.css
CHANGED
|
@@ -82,6 +82,20 @@
|
|
|
82
82
|
min-height: var(--cn-field-height);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/* NcTextField draws its input inside `.input-field__main-wrapper`, which has a
|
|
86
|
+
FIXED height (`--default-clickable-area`, 34px on Nextcloud 34) and a 2px
|
|
87
|
+
padding ring for the focus outline. The min-height above makes the input
|
|
88
|
+
40px, so without this the input overflowed its wrapper by 6px and the help
|
|
89
|
+
line under it, which starts at the wrapper's bottom edge, sat behind the
|
|
90
|
+
input's lower border. NcSelect grows with its content, which is why only
|
|
91
|
+
plain inputs showed it. The wrapper now takes the field height plus its
|
|
92
|
+
padding ring, so the input fits and the help line starts below it.
|
|
93
|
+
@spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-help-text-starts-below-every-input */
|
|
94
|
+
.cn-look-board .cn-form-dialog__field .input-field__main-wrapper,
|
|
95
|
+
.cn-look-board .cn-form-page__field .input-field__main-wrapper {
|
|
96
|
+
height: calc(var(--cn-field-height) + 2 * var(--border-width-input-focused, 2px));
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
.cn-look-board .cn-form-dialog__field textarea,
|
|
86
100
|
.cn-look-board .cn-form-page__field textarea {
|
|
87
101
|
padding: 10px 12px;
|
package/src/css/index-page.css
CHANGED
|
@@ -240,3 +240,74 @@
|
|
|
240
240
|
color: var(--color-text-maxcontrast);
|
|
241
241
|
font-size: 0.9em;
|
|
242
242
|
}
|
|
243
|
+
|
|
244
|
+
/* An index page with cards beside its list (`sidePanel`,
|
|
245
|
+
screens-index-stat-row): the header rows span the page, the toolbar and the
|
|
246
|
+
list take the first column and the side panel the second (300px, 20px
|
|
247
|
+
apart), spanning both. Every other child (dialogs, the inline sidebar) sits
|
|
248
|
+
in a zero-height area after them. The list row is the flexible one, so a
|
|
249
|
+
side panel taller than the toolbar never opens a gap under the toolbar.
|
|
250
|
+
Below 1024px the panel moves above the toolbar. */
|
|
251
|
+
.cn-index-page--with-side-panel {
|
|
252
|
+
display: grid;
|
|
253
|
+
grid-template-columns: minmax(0, 1fr) var(--cn-index-side-panel-width, 300px);
|
|
254
|
+
grid-template-rows: auto auto auto auto minmax(0, 1fr) auto;
|
|
255
|
+
grid-template-areas:
|
|
256
|
+
"head head"
|
|
257
|
+
"below below"
|
|
258
|
+
"stats stats"
|
|
259
|
+
"bar side"
|
|
260
|
+
"body side"
|
|
261
|
+
"extra extra";
|
|
262
|
+
column-gap: 20px;
|
|
263
|
+
align-content: start;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.cn-index-page--with-side-panel > * {
|
|
267
|
+
grid-area: extra;
|
|
268
|
+
min-width: 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.cn-index-page--with-side-panel > :first-child {
|
|
272
|
+
grid-area: head;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.cn-index-page--with-side-panel > .cn-index-page__below-header {
|
|
276
|
+
grid-area: below;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.cn-index-page--with-side-panel > .cn-index-page__stat-row {
|
|
280
|
+
grid-area: stats;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.cn-index-page--with-side-panel > .cn-actions-bar {
|
|
284
|
+
grid-area: bar;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.cn-index-page--with-side-panel > .cn-index-page__body {
|
|
288
|
+
grid-area: body;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.cn-index-page--with-side-panel > .cn-index-page__side-panel {
|
|
292
|
+
grid-area: side;
|
|
293
|
+
align-self: start;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@media (max-width: 1024px) {
|
|
297
|
+
.cn-index-page--with-side-panel {
|
|
298
|
+
grid-template-columns: minmax(0, 1fr);
|
|
299
|
+
grid-template-areas:
|
|
300
|
+
"head"
|
|
301
|
+
"below"
|
|
302
|
+
"stats"
|
|
303
|
+
"side"
|
|
304
|
+
"bar"
|
|
305
|
+
"body"
|
|
306
|
+
"extra";
|
|
307
|
+
grid-template-rows: auto auto auto auto auto minmax(0, 1fr) auto;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.cn-index-page--with-side-panel > .cn-index-page__side-panel {
|
|
311
|
+
margin-bottom: 20px;
|
|
312
|
+
}
|
|
313
|
+
}
|
package/src/css/kpi-card.css
CHANGED
|
@@ -450,6 +450,38 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
450
450
|
font-weight: 400;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
/* A caption on a line of its own wraps, up to two lines (DqDashboard: "haalden
|
|
454
|
+
hun termijn, naar doorlooptijd" runs over two lines in a narrow tile). The
|
|
455
|
+
one-line rule of `.cn-kpi-card__label` is for a caption BESIDE the number,
|
|
456
|
+
where a second line would push the tile taller; this line has the width of
|
|
457
|
+
the tile to itself. The clamp is safe here because the element is a plain
|
|
458
|
+
span, not a flex row (compare the title note above).
|
|
459
|
+
@spec openspec/changes/dashboard-tile-text-fits/specs/dashboard-tile-text-fits/spec.md#requirement-a-kpi-tiles-caption-and-board-label-wrap-to-two-lines */
|
|
460
|
+
.cn-kpi-card--stacked .cn-kpi-card__caption-line {
|
|
461
|
+
display: -webkit-box;
|
|
462
|
+
-webkit-box-orient: vertical;
|
|
463
|
+
-webkit-line-clamp: 2;
|
|
464
|
+
line-clamp: 2;
|
|
465
|
+
white-space: normal;
|
|
466
|
+
overflow-wrap: break-word;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Board look: the label wraps too, up to two lines, beside its icon badge
|
|
470
|
+
("Afgehandeld deze maand"). The clamp sits on the label's own span
|
|
471
|
+
(`cn-kpi-card__title-text`), not on the title row, which stays the flex row
|
|
472
|
+
that carries the badge or the range select. */
|
|
473
|
+
.cn-kpi-card--board .cn-kpi-card__title-text {
|
|
474
|
+
flex: 1 1 auto;
|
|
475
|
+
min-width: 0;
|
|
476
|
+
display: -webkit-box;
|
|
477
|
+
-webkit-box-orient: vertical;
|
|
478
|
+
-webkit-line-clamp: 2;
|
|
479
|
+
line-clamp: 2;
|
|
480
|
+
overflow: hidden;
|
|
481
|
+
white-space: normal;
|
|
482
|
+
overflow-wrap: break-word;
|
|
483
|
+
}
|
|
484
|
+
|
|
453
485
|
/* The 18px glyph before the label, drawn only on a link tile. */
|
|
454
486
|
.cn-kpi-card--board .cn-kpi-card__title {
|
|
455
487
|
display: flex;
|
|
@@ -87,6 +87,61 @@
|
|
|
87
87
|
margin-bottom: 0;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/* The breadcrumb is a line of 14px text between the pills and the meta, as
|
|
91
|
+
the boards draw it (screens-detail-header-row-parity). NcBreadcrumbs grows to
|
|
92
|
+
the full row and draws each crumb as a 34px tertiary button, which pushed the
|
|
93
|
+
pills above it and the meta under it. The doubled class outranks its scoped
|
|
94
|
+
rules. */
|
|
95
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs.cn-detail-page__breadcrumbs {
|
|
96
|
+
flex: 0 1 auto;
|
|
97
|
+
height: auto;
|
|
98
|
+
min-height: 0;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
width: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .breadcrumb__crumbs {
|
|
104
|
+
gap: 8px;
|
|
105
|
+
margin: 0;
|
|
106
|
+
min-height: 0;
|
|
107
|
+
padding: 0;
|
|
108
|
+
vertical-align: middle;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb.vue-crumb {
|
|
112
|
+
gap: 8px;
|
|
113
|
+
height: auto;
|
|
114
|
+
min-height: 0;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb .button-vue.button-vue {
|
|
119
|
+
background: transparent;
|
|
120
|
+
border: 0;
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
font-weight: 400;
|
|
123
|
+
height: auto;
|
|
124
|
+
min-height: 0;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb .button-vue__text {
|
|
130
|
+
font-weight: inherit;
|
|
131
|
+
line-height: 20px;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The list is the link; the record's own crumb is plain muted text. */
|
|
136
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb .button-vue[aria-current='page'] {
|
|
137
|
+
color: var(--color-text-maxcontrast);
|
|
138
|
+
cursor: default;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.cn-look-board .cn-detail-page__header-row2 .cn-detail-page__breadcrumbs .vue-crumb__separator {
|
|
142
|
+
margin: 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
90
145
|
.cn-look-board .cn-detail-page__header-dot,
|
|
91
146
|
.cn-look-board .cn-detail-page__header-meta {
|
|
92
147
|
color: var(--color-text-maxcontrast);
|
|
@@ -112,6 +167,16 @@
|
|
|
112
167
|
gap: 20px;
|
|
113
168
|
}
|
|
114
169
|
|
|
170
|
+
/* GridStack insets every cell by its 12px margin, the outer edge included, so
|
|
171
|
+
the tab strip started 12px below the top of the body column and stopped 12px
|
|
172
|
+
short of its end, while the side column started at the top. Pulling the grid
|
|
173
|
+
out by that margin puts its first card on the column's edges: the side column
|
|
174
|
+
is level with the strip, and the strip ends where the body column ends. */
|
|
175
|
+
.cn-look-board.cn-detail-page--with-side > .cn-detail-page__body > .cn-detail-page__grid {
|
|
176
|
+
margin: -12px;
|
|
177
|
+
width: auto;
|
|
178
|
+
}
|
|
179
|
+
|
|
115
180
|
/* ---- Folder tabs ------------------------------------------------------- */
|
|
116
181
|
|
|
117
182
|
/* The strip draws no line of its own (design-system pull request 147). The card under it
|
|
@@ -219,6 +284,22 @@
|
|
|
219
284
|
border-radius: 0;
|
|
220
285
|
}
|
|
221
286
|
|
|
287
|
+
/* A tab that holds a stack (`widgetIds`, screens-relation-pages): the panel
|
|
288
|
+
draws no frame and no padding; each widget is its own card. The first card
|
|
289
|
+
joins the open tab (the first-card rule above), the next ones stand 20px
|
|
290
|
+
apart, as the Overzicht of DqContact and PqOrganisatie draws them. */
|
|
291
|
+
.cn-look-board .cn-tabs--board.cn-tabs-widget__tabs .cn-tabs__content:has(> .cn-tab:not([hidden]) > .cn-tabs-widget__stack) {
|
|
292
|
+
background: transparent;
|
|
293
|
+
border: 0;
|
|
294
|
+
border-radius: 0;
|
|
295
|
+
padding: 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* The stack's next cards are whole cards, not joined to the strip. */
|
|
299
|
+
.cn-look-board .cn-tabs--board .cn-tabs__content .cn-tab .cn-tabs-widget__stack-item .cn-widget-wrapper {
|
|
300
|
+
border-radius: var(--cn-board-card-radius);
|
|
301
|
+
}
|
|
302
|
+
|
|
222
303
|
/* ---- Body cards and side cards ----------------------------------------- */
|
|
223
304
|
|
|
224
305
|
.cn-look-board .cn-detail-page .cn-widget-wrapper,
|
|
@@ -298,6 +379,12 @@
|
|
|
298
379
|
display: none;
|
|
299
380
|
}
|
|
300
381
|
|
|
382
|
+
/* A side card whose widget reports a header action (screens-relation-pages:
|
|
383
|
+
the "+ Toevoegen" of Contactkanalen and Relaties) keeps that one button. */
|
|
384
|
+
.cn-look-board .cn-detail-page__side .cn-widget-wrapper__actions:has(.cn-detail-page__catalog-action) {
|
|
385
|
+
display: flex;
|
|
386
|
+
}
|
|
387
|
+
|
|
301
388
|
/* The notice at the top of the side column: radius 12, padding 14px 16px. */
|
|
302
389
|
.cn-look-board .cn-detail-page__side-item--notice .cn-widget-wrapper,
|
|
303
390
|
.cn-look-board .cn-detail-page__side-item--notice .cn-banner-widget,
|