@conduction/nextcloud-vue 2.76.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/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/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 +16 -28
- 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 +98 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
- package/dist/esm/components/CnIndexPage/CnIndexPage.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/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/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/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +25 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +25 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +288 -74
- 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 +17 -0
- 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/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 +3873 -2674
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +288 -74
- package/l10n/en.json +25 -1
- package/l10n/nl.json +25 -1
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +80 -20
- package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
- 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 +36 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +106 -19
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/useListView.js +4 -0
- package/src/css/look-board-detail.css +40 -2
- package/src/css/table.css +82 -17
- package/src/index.js +2 -1
- package/src/schemas/app-manifest-v2.schema.json +11 -7
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +17 -0
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +1 -1
- package/src/utils/lensAvailability.js +86 -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
|
@@ -533,7 +533,8 @@
|
|
|
533
533
|
<!-- Empty state -->
|
|
534
534
|
<div v-else-if="effectiveObjects.length === 0" class="cn-index-page__empty">
|
|
535
535
|
<slot name="empty">
|
|
536
|
-
<CnEmptyContent :name="resolvedEmptyText"
|
|
536
|
+
<CnEmptyContent :name="lensReasonText || resolvedEmptyText"
|
|
537
|
+
:data-lens-unavailable="lensReasonText ? 'true' : null">
|
|
537
538
|
<template #icon>
|
|
538
539
|
<CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
|
|
539
540
|
<DatabaseSearch v-else :size="64" />
|
|
@@ -573,14 +574,15 @@
|
|
|
573
574
|
@rowClick="onRowClick"
|
|
574
575
|
@rowAuxClick="onRowAuxClick"
|
|
575
576
|
@rowContextMenu="onRowContextMenu">
|
|
576
|
-
<!--
|
|
577
|
-
<template v-if="
|
|
578
|
-
<
|
|
579
|
-
v-if="row && row['@self'] && typeof row['@self'].
|
|
577
|
+
<!-- Follow column (showFollowColumn, or the deprecated showFavouriteColumn) -->
|
|
578
|
+
<template v-if="followColumnOn" #column-__follow="{ row }">
|
|
579
|
+
<CnFollowToggle
|
|
580
|
+
v-if="row && row['@self'] && typeof row['@self'].watching === 'boolean'"
|
|
581
|
+
compact
|
|
580
582
|
:register="typeof register === 'string' ? register : ''"
|
|
581
583
|
:schema="favouriteSchemaSlug"
|
|
582
584
|
:objectId="String(row['@self'].id || row.id || '')"
|
|
583
|
-
:
|
|
585
|
+
:watching="row['@self'].watching === true" />
|
|
584
586
|
</template>
|
|
585
587
|
|
|
586
588
|
<!-- Pass through column slots -->
|
|
@@ -955,7 +957,7 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
|
955
957
|
import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
|
|
956
958
|
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
957
959
|
import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
|
|
958
|
-
import
|
|
960
|
+
import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
|
|
959
961
|
import { useContextMenu } from '../../composables/index.js'
|
|
960
962
|
import { useLook } from '../../composables/useLook.js'
|
|
961
963
|
import { copyKindsOf } from '../../composables/useObjectCopy.js'
|
|
@@ -968,6 +970,7 @@ import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actio
|
|
|
968
970
|
import { reportBindingProblems } from '../../utils/diagnostics.js'
|
|
969
971
|
import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js'
|
|
970
972
|
import { buildExportUrl } from '../../utils/indexExportHelpers.js'
|
|
973
|
+
import { lensUnavailableText } from '../../utils/lensAvailability.js'
|
|
971
974
|
import { openRowTarget } from '../../utils/linkNavigation.js'
|
|
972
975
|
import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
|
|
973
976
|
import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
|
|
@@ -1037,8 +1040,8 @@ const CALENDAR_PAGE_SIZE = 200
|
|
|
1037
1040
|
*/
|
|
1038
1041
|
const RANGE_SEPARATOR = '..'
|
|
1039
1042
|
|
|
1040
|
-
/** Key of the synthetic
|
|
1041
|
-
const
|
|
1043
|
+
/** Key of the synthetic follow column (`showFollowColumn`). */
|
|
1044
|
+
const FOLLOW_COLUMN_KEY = '__follow'
|
|
1042
1045
|
|
|
1043
1046
|
/**
|
|
1044
1047
|
* Whether a schema property wants a from/to pair rather than a value list.
|
|
@@ -1240,7 +1243,7 @@ export default {
|
|
|
1240
1243
|
|
|
1241
1244
|
components: {
|
|
1242
1245
|
CnBuildiqEditButton,
|
|
1243
|
-
|
|
1246
|
+
CnFollowToggle,
|
|
1244
1247
|
NcLoadingIcon,
|
|
1245
1248
|
CnEmptyContent,
|
|
1246
1249
|
NcActions,
|
|
@@ -1576,9 +1579,10 @@ export default {
|
|
|
1576
1579
|
},
|
|
1577
1580
|
|
|
1578
1581
|
/**
|
|
1579
|
-
* Personal lenses appended to the quick filters: any of `
|
|
1580
|
-
* (`
|
|
1581
|
-
*
|
|
1582
|
+
* Personal lenses appended to the quick filters: any of `watching`
|
|
1583
|
+
* (`_watching`, labelled Following), `recent` (`_recent`) and `unread`
|
|
1584
|
+
* (`_unread`). `favourite` is a deprecated alias of `watching`: a
|
|
1585
|
+
* favourite is a follow now, and asking for both gives one tab.
|
|
1582
1586
|
* They combine with every other filter. While Recent is active column
|
|
1583
1587
|
* sorting is off, because the lens owns the order. Manifest
|
|
1584
1588
|
* `config.personalLenses`.
|
|
@@ -1591,9 +1595,20 @@ export default {
|
|
|
1591
1595
|
},
|
|
1592
1596
|
|
|
1593
1597
|
/**
|
|
1594
|
-
* Add a first column with a
|
|
1595
|
-
* each row's `@self.
|
|
1596
|
-
* Manifest `config.
|
|
1598
|
+
* Add a first column with a follow toggle per row (`CnFollowToggle`,
|
|
1599
|
+
* compact), bound to each row's `@self.watching`. Clicking it does not
|
|
1600
|
+
* open the row. Manifest `config.showFollowColumn`.
|
|
1601
|
+
*/
|
|
1602
|
+
showFollowColumn: {
|
|
1603
|
+
type: Boolean,
|
|
1604
|
+
default: false,
|
|
1605
|
+
},
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Deprecated alias of `showFollowColumn`: the star column became the
|
|
1609
|
+
* follow column (OpenRegister `merge-follow-and-favourites`).
|
|
1610
|
+
*
|
|
1611
|
+
* @deprecated Use `showFollowColumn`.
|
|
1597
1612
|
*/
|
|
1598
1613
|
showFavouriteColumn: {
|
|
1599
1614
|
type: Boolean,
|
|
@@ -2088,6 +2103,34 @@ export default {
|
|
|
2088
2103
|
default: 'No items found',
|
|
2089
2104
|
},
|
|
2090
2105
|
|
|
2106
|
+
/**
|
|
2107
|
+
* Personal-lens report of the list on screen, for a host that fetches
|
|
2108
|
+
* itself: the response's `@self.lenses` (`{ recent: { available,
|
|
2109
|
+
* reason } }`). In self-fetch mode the page reads it from the store
|
|
2110
|
+
* and this prop is ignored. When a lens reports `available: false`
|
|
2111
|
+
* the empty state explains why instead of showing `emptyText`.
|
|
2112
|
+
*
|
|
2113
|
+
* @type {object|null}
|
|
2114
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
2115
|
+
*/
|
|
2116
|
+
lenses: {
|
|
2117
|
+
type: Object,
|
|
2118
|
+
default: null,
|
|
2119
|
+
},
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* App wording for an unavailable lens, keyed `<lens>.<reason>`
|
|
2123
|
+
* (`recent.audit-trail-disabled`) or `<reason>`; the specific key
|
|
2124
|
+
* wins, and both win over the library's object-neutral text.
|
|
2125
|
+
*
|
|
2126
|
+
* @type {{[key: string]: string}|null}
|
|
2127
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
2128
|
+
*/
|
|
2129
|
+
lensReasonTexts: {
|
|
2130
|
+
type: Object,
|
|
2131
|
+
default: null,
|
|
2132
|
+
},
|
|
2133
|
+
|
|
2091
2134
|
/** Accessible label for the loading spinner (NcLoadingIcon aria-label) */
|
|
2092
2135
|
loadingText: {
|
|
2093
2136
|
type: String,
|
|
@@ -3279,6 +3322,40 @@ export default {
|
|
|
3279
3322
|
return this.emptyText ? fn(this.emptyText) : this.emptyText
|
|
3280
3323
|
},
|
|
3281
3324
|
|
|
3325
|
+
/**
|
|
3326
|
+
* The personal-lens reports of the list on screen: the store's copy
|
|
3327
|
+
* of the latest response in self-fetch mode, else the `lenses` prop.
|
|
3328
|
+
*
|
|
3329
|
+
* @return {object}
|
|
3330
|
+
* @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
|
|
3331
|
+
*/
|
|
3332
|
+
effectiveLenses() {
|
|
3333
|
+
if (this.isNamedSource) {
|
|
3334
|
+
return {}
|
|
3335
|
+
}
|
|
3336
|
+
if (this.isSelfFetchMode) {
|
|
3337
|
+
return (this.list.lenses && this.list.lenses.value) || {}
|
|
3338
|
+
}
|
|
3339
|
+
return this.lenses || {}
|
|
3340
|
+
},
|
|
3341
|
+
|
|
3342
|
+
/**
|
|
3343
|
+
* Why the list is empty when a personal lens could not answer (the
|
|
3344
|
+
* audit trail is off, nobody is logged in, the history could not be
|
|
3345
|
+
* read), or `''` to keep the generic empty text.
|
|
3346
|
+
*
|
|
3347
|
+
* @return {string}
|
|
3348
|
+
* @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
|
|
3349
|
+
*/
|
|
3350
|
+
lensReasonText() {
|
|
3351
|
+
const text = lensUnavailableText(this.effectiveLenses, this.lensReasonTexts)
|
|
3352
|
+
if (!text) {
|
|
3353
|
+
return ''
|
|
3354
|
+
}
|
|
3355
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
3356
|
+
return fn(text)
|
|
3357
|
+
},
|
|
3358
|
+
|
|
3282
3359
|
/**
|
|
3283
3360
|
* Whether the host manifest editor is in edit mode (unwraps the injected
|
|
3284
3361
|
* `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the
|
|
@@ -4172,12 +4249,22 @@ export default {
|
|
|
4172
4249
|
if (this.recentLensActive) {
|
|
4173
4250
|
cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col))
|
|
4174
4251
|
}
|
|
4175
|
-
if (this.
|
|
4176
|
-
cols = [{ key:
|
|
4252
|
+
if (this.followColumnOn && this.favouriteSchemaSlug !== '') {
|
|
4253
|
+
cols = [{ key: FOLLOW_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols]
|
|
4177
4254
|
}
|
|
4178
4255
|
return cols
|
|
4179
4256
|
},
|
|
4180
4257
|
|
|
4258
|
+
/**
|
|
4259
|
+
* Whether the follow column renders (`showFollowColumn`, or the deprecated `showFavouriteColumn`).
|
|
4260
|
+
*
|
|
4261
|
+
* @return {boolean}
|
|
4262
|
+
* @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-index-pages-offer-one-following-lens-and-a-follow-column
|
|
4263
|
+
*/
|
|
4264
|
+
followColumnOn() {
|
|
4265
|
+
return this.showFollowColumn || this.showFavouriteColumn
|
|
4266
|
+
},
|
|
4267
|
+
|
|
4181
4268
|
/**
|
|
4182
4269
|
* Whether the active quick filter is the Recent lens.
|
|
4183
4270
|
*
|
|
@@ -4192,7 +4279,7 @@ export default {
|
|
|
4192
4279
|
},
|
|
4193
4280
|
|
|
4194
4281
|
/**
|
|
4195
|
-
* Schema slug the
|
|
4282
|
+
* Schema slug the follow column calls with: the page's own slug in self-fetch
|
|
4196
4283
|
* mode, else the schema object's `slug`. Empty when neither is known.
|
|
4197
4284
|
*
|
|
4198
4285
|
* @return {string}
|
|
@@ -149,6 +149,28 @@
|
|
|
149
149
|
@update:modelValue="onRelationChange(field, $event)"
|
|
150
150
|
@close="commitEdit" />
|
|
151
151
|
|
|
152
|
+
<!-- Nextcloud group or user (`referenceType: nextcloud-group`
|
|
153
|
+
/ `nextcloud-user`, one or a list): pick by display name,
|
|
154
|
+
store the group id or uid. The same search the create and
|
|
155
|
+
edit dialogs use. -->
|
|
156
|
+
<component
|
|
157
|
+
:is="isUserPicker(field) ? 'NcSelectUsers' : 'NcSelect'"
|
|
158
|
+
v-else-if="isPersonPicker(field)"
|
|
159
|
+
ref="activeEditor"
|
|
160
|
+
:inputLabel="field.label"
|
|
161
|
+
:labelOutside="true"
|
|
162
|
+
:options="personOptions[field.key] || []"
|
|
163
|
+
:modelValue="personSelected(field)"
|
|
164
|
+
:multiple="isPersonMultiple(field)"
|
|
165
|
+
:keepOpen="isPersonMultiple(field)"
|
|
166
|
+
:loading="personLoading === field.key"
|
|
167
|
+
:filterable="false"
|
|
168
|
+
:clearable="!field.required"
|
|
169
|
+
label="label"
|
|
170
|
+
@search="(q) => searchPeople(field, q)"
|
|
171
|
+
@update:modelValue="onPersonChange(field, $event)"
|
|
172
|
+
@close="isPersonMultiple(field) ? undefined : commitEdit()" />
|
|
173
|
+
|
|
152
174
|
<!-- Select -->
|
|
153
175
|
<NcSelect
|
|
154
176
|
v-else-if="field.widget === 'select'"
|
|
@@ -259,7 +281,17 @@
|
|
|
259
281
|
:value="displayValues[field.key]"
|
|
260
282
|
:raw="(objectData || {})[field.key]" />
|
|
261
283
|
<template v-else>
|
|
262
|
-
|
|
284
|
+
<!-- A Nextcloud group (or list): its display name, cached
|
|
285
|
+
per id; the id while the name loads or when unknown. -->
|
|
286
|
+
<span
|
|
287
|
+
v-if="isGroupPicker(field) && groupIdsOf(field).length"
|
|
288
|
+
class="cn-object-data-widget__groups">
|
|
289
|
+
<template v-for="(gid, gi) in groupIdsOf(field)" :key="gid">
|
|
290
|
+
<span v-if="gi > 0">, </span>
|
|
291
|
+
<CnGroupNameCell :gid="gid" />
|
|
292
|
+
</template>
|
|
293
|
+
</span>
|
|
294
|
+
<img v-else-if="isImageField(field) && rawOf(field)"
|
|
263
295
|
:src="rawOf(field)"
|
|
264
296
|
:alt="field.label"
|
|
265
297
|
class="cn-object-data-widget__image">
|
|
@@ -370,19 +402,22 @@
|
|
|
370
402
|
import axios from '@nextcloud/axios'
|
|
371
403
|
import { translate as t } from '@nextcloud/l10n'
|
|
372
404
|
import { generateUrl } from '@nextcloud/router'
|
|
373
|
-
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
405
|
+
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcSelectUsers, NcTextField } from '@nextcloud/vue'
|
|
374
406
|
import Check from 'vue-material-design-icons/Check.vue'
|
|
375
407
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
376
408
|
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
377
409
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
378
410
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
411
|
+
import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
|
|
379
412
|
import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
|
|
380
413
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
381
414
|
import { useObjectStore } from '../../store/index.js'
|
|
415
|
+
import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
382
416
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
383
417
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
384
418
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
385
419
|
import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
|
|
420
|
+
import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
|
|
386
421
|
import { CnIcon } from '../CnIcon/index.js'
|
|
387
422
|
import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
|
|
388
423
|
import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
|
|
@@ -427,6 +462,8 @@ export default {
|
|
|
427
462
|
NcLoadingIcon,
|
|
428
463
|
NcTextField,
|
|
429
464
|
NcSelect,
|
|
465
|
+
NcSelectUsers,
|
|
466
|
+
CnGroupNameCell,
|
|
430
467
|
NcCheckboxRadioSwitch,
|
|
431
468
|
NcActionButton,
|
|
432
469
|
CnWidgetWrapper,
|
|
@@ -806,6 +843,12 @@ export default {
|
|
|
806
843
|
relationOptions: {},
|
|
807
844
|
/** Whether relation picker options are being fetched. */
|
|
808
845
|
relationOptionsLoading: false,
|
|
846
|
+
/** Group and user picker options per field key ({ id, label }[]). */
|
|
847
|
+
personOptions: {},
|
|
848
|
+
/** Key of the group or user field whose options are loading, or null. */
|
|
849
|
+
personLoading: null,
|
|
850
|
+
/** Display names of users seen in a user picker, by uid. */
|
|
851
|
+
userLabels: {},
|
|
809
852
|
/** Whether the user expanded the widget to see every field. */
|
|
810
853
|
expanded: false,
|
|
811
854
|
/** Fields that fit the cell while collapsed; null when all fit. */
|
|
@@ -1719,6 +1762,11 @@ export default {
|
|
|
1719
1762
|
if (this.isSingleRelationField(field.key)) {
|
|
1720
1763
|
this.loadRelationOptions(field.key)
|
|
1721
1764
|
}
|
|
1765
|
+
// Group and user fields pick from Nextcloud's own directory.
|
|
1766
|
+
if (this.isPersonPicker(field)) {
|
|
1767
|
+
this.searchPeople(field, '')
|
|
1768
|
+
this.resolvePersonLabels(field, currentValue)
|
|
1769
|
+
}
|
|
1722
1770
|
|
|
1723
1771
|
this.$nextTick(() => {
|
|
1724
1772
|
// Focus the editor
|
|
@@ -1944,6 +1992,182 @@ export default {
|
|
|
1944
1992
|
return options.find((opt) => opt.id === val) || { id: val, label: String(val) }
|
|
1945
1993
|
},
|
|
1946
1994
|
|
|
1995
|
+
/**
|
|
1996
|
+
* Whether a field picks a Nextcloud group (widget `group` or
|
|
1997
|
+
* `group-multiselect`, from `referenceType: nextcloud-group`).
|
|
1998
|
+
*
|
|
1999
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2000
|
+
* @param {object} field Field descriptor.
|
|
2001
|
+
* @return {boolean}
|
|
2002
|
+
*/
|
|
2003
|
+
isGroupPicker(field) {
|
|
2004
|
+
return !!field && (field.widget === 'group' || field.widget === 'group-multiselect')
|
|
2005
|
+
},
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Whether a field picks a Nextcloud user (widget `user` or
|
|
2009
|
+
* `user-multiselect`, from `referenceType: nextcloud-user`).
|
|
2010
|
+
*
|
|
2011
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2012
|
+
* @param {object} field Field descriptor.
|
|
2013
|
+
* @return {boolean}
|
|
2014
|
+
*/
|
|
2015
|
+
isUserPicker(field) {
|
|
2016
|
+
return !!field && (field.widget === 'user' || field.widget === 'user-multiselect')
|
|
2017
|
+
},
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Whether a field picks a group or a user.
|
|
2021
|
+
*
|
|
2022
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2023
|
+
* @param {object} field Field descriptor.
|
|
2024
|
+
* @return {boolean}
|
|
2025
|
+
*/
|
|
2026
|
+
isPersonPicker(field) {
|
|
2027
|
+
return this.isGroupPicker(field) || this.isUserPicker(field)
|
|
2028
|
+
},
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Whether a group or user field holds a list.
|
|
2032
|
+
*
|
|
2033
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2034
|
+
* @param {object} field Field descriptor.
|
|
2035
|
+
* @return {boolean}
|
|
2036
|
+
*/
|
|
2037
|
+
isPersonMultiple(field) {
|
|
2038
|
+
return !!field && (field.widget === 'group-multiselect' || field.widget === 'user-multiselect')
|
|
2039
|
+
},
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* The group ids a group field holds, for the display mode.
|
|
2043
|
+
*
|
|
2044
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2045
|
+
* @param {object} field Field descriptor.
|
|
2046
|
+
* @return {string[]}
|
|
2047
|
+
*/
|
|
2048
|
+
groupIdsOf(field) {
|
|
2049
|
+
const raw = this.rawOf(field)
|
|
2050
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
2051
|
+
return list.filter((v) => typeof v === 'string' && v !== '')
|
|
2052
|
+
},
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* The display name of a group id or uid, falling back to the id.
|
|
2056
|
+
*
|
|
2057
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2058
|
+
* @param {object} field Field descriptor.
|
|
2059
|
+
* @param {string} id The group id or uid.
|
|
2060
|
+
* @return {string}
|
|
2061
|
+
*/
|
|
2062
|
+
personLabel(field, id) {
|
|
2063
|
+
if (this.isGroupPicker(field)) {
|
|
2064
|
+
return groupDisplayName(id) || String(id)
|
|
2065
|
+
}
|
|
2066
|
+
return this.userLabels[id] || String(id)
|
|
2067
|
+
},
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* Search Nextcloud groups or users for a picker. Fails soft (the
|
|
2071
|
+
* helpers return `[]`), so the picker stays usable with what it has.
|
|
2072
|
+
*
|
|
2073
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2074
|
+
* @param {object} field Field descriptor.
|
|
2075
|
+
* @param {string} query The search term.
|
|
2076
|
+
* @return {Promise<void>}
|
|
2077
|
+
*/
|
|
2078
|
+
async searchPeople(field, query) {
|
|
2079
|
+
this.personLoading = field.key
|
|
2080
|
+
try {
|
|
2081
|
+
const options = this.isGroupPicker(field)
|
|
2082
|
+
? await searchNextcloudGroups(query || '')
|
|
2083
|
+
: await searchNextcloudUsers(query || '')
|
|
2084
|
+
const list = Array.isArray(options) ? options : []
|
|
2085
|
+
if (this.isUserPicker(field)) {
|
|
2086
|
+
const labels = { ...this.userLabels }
|
|
2087
|
+
for (const opt of list) {
|
|
2088
|
+
labels[opt.id] = opt.label
|
|
2089
|
+
}
|
|
2090
|
+
this.userLabels = labels
|
|
2091
|
+
}
|
|
2092
|
+
this.personOptions = { ...this.personOptions, [field.key]: list }
|
|
2093
|
+
} finally {
|
|
2094
|
+
if (this.personLoading === field.key) {
|
|
2095
|
+
this.personLoading = null
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
},
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Look up the display names of the value a group or user field holds,
|
|
2102
|
+
* so the open editor shows names rather than ids.
|
|
2103
|
+
*
|
|
2104
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2105
|
+
* @param {object} field Field descriptor.
|
|
2106
|
+
* @param {unknown} value The held value (an id or a list of ids).
|
|
2107
|
+
* @return {Promise<void>}
|
|
2108
|
+
*/
|
|
2109
|
+
async resolvePersonLabels(field, value) {
|
|
2110
|
+
const ids = (Array.isArray(value) ? value : [value])
|
|
2111
|
+
.filter((v) => typeof v === 'string' && v !== '')
|
|
2112
|
+
if (this.isGroupPicker(field)) {
|
|
2113
|
+
await Promise.all(ids.map((gid) => loadGroupDisplayName(gid)))
|
|
2114
|
+
return
|
|
2115
|
+
}
|
|
2116
|
+
const unknown = ids.filter((uid) => !this.userLabels[uid])
|
|
2117
|
+
const options = await Promise.all(unknown.map((uid) => resolveNextcloudUser(uid)))
|
|
2118
|
+
if (options.length > 0) {
|
|
2119
|
+
const labels = { ...this.userLabels }
|
|
2120
|
+
for (const opt of options) {
|
|
2121
|
+
if (opt && opt.id) {
|
|
2122
|
+
labels[opt.id] = opt.label || opt.id
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
this.userLabels = labels
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* The selected option(s) of a group or user picker.
|
|
2131
|
+
*
|
|
2132
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2133
|
+
* @param {object} field Field descriptor.
|
|
2134
|
+
* @return {object|object[]|null}
|
|
2135
|
+
*/
|
|
2136
|
+
personSelected(field) {
|
|
2137
|
+
const toOption = (id) => {
|
|
2138
|
+
const label = this.personLabel(field, id)
|
|
2139
|
+
return { id, label, displayName: label }
|
|
2140
|
+
}
|
|
2141
|
+
const v = this.editData[field.key]
|
|
2142
|
+
if (this.isPersonMultiple(field)) {
|
|
2143
|
+
return (Array.isArray(v) ? v : [])
|
|
2144
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2145
|
+
.map(toOption)
|
|
2146
|
+
}
|
|
2147
|
+
return (typeof v === 'string' && v !== '') ? toOption(v) : null
|
|
2148
|
+
},
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Store a group or user choice as the id (or list of ids).
|
|
2152
|
+
*
|
|
2153
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2154
|
+
* @param {object} field Field descriptor.
|
|
2155
|
+
* @param {object|object[]|null} selected The chosen option(s).
|
|
2156
|
+
*/
|
|
2157
|
+
onPersonChange(field, selected) {
|
|
2158
|
+
if (this.isPersonMultiple(field)) {
|
|
2159
|
+
const ids = (Array.isArray(selected) ? selected : [])
|
|
2160
|
+
.map((opt) => (opt && typeof opt === 'object' ? opt.id : opt))
|
|
2161
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2162
|
+
this.updateField(field.key, ids)
|
|
2163
|
+
return
|
|
2164
|
+
}
|
|
2165
|
+
if (selected && this.isUserPicker(field) && selected.id) {
|
|
2166
|
+
this.userLabels = { ...this.userLabels, [selected.id]: selected.label || selected.id }
|
|
2167
|
+
}
|
|
2168
|
+
this.updateField(field.key, selected && selected.id ? selected.id : null)
|
|
2169
|
+
},
|
|
2170
|
+
|
|
1947
2171
|
onSelectChange(field, option) {
|
|
1948
2172
|
this.updateField(field.key, option ? option.id : null)
|
|
1949
2173
|
},
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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,
|
|
@@ -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,
|