@conduction/nextcloud-vue 2.106.0 → 2.107.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/CnActionDialog/CnActionDialog.vue.js +9 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js.map +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +338 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +249 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js +2 -2
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +145 -138
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +102 -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/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +83 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +4 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +389 -20
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -35
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +69 -5
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +38 -5
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +7 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +8 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +297 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +24 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js +9 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js +95 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js +123 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue.js +1 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js +13 -5
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js +3 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue.js +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js +49 -23
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js +74 -27
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js +0 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +18 -413
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +27 -214
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js +9 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js +190 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js +270 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +56 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +56 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/savedViewsList.js +235 -0
- package/dist/esm/mixins/savedViewsList.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +348 -108
- package/dist/esm/utils/actionsDispatcher.js +2 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/builtInModals.js +39 -0
- package/dist/esm/utils/builtInModals.js.map +1 -0
- package/dist/esm/utils/mapMarkerColor.js +121 -0
- package/dist/esm/utils/mapMarkerColor.js.map +1 -0
- package/dist/esm/utils/mergeManifestDelta.js +4 -0
- package/dist/esm/utils/mergeManifestDelta.js.map +1 -1
- package/dist/esm/utils/savedViewHelpers.js +59 -7
- package/dist/esm/utils/savedViewHelpers.js.map +1 -1
- package/dist/esm/utils/savedViewMenu.js +154 -0
- package/dist/esm/utils/savedViewMenu.js.map +1 -0
- package/dist/esm/utils/searchGroupSharees.js +50 -15
- package/dist/esm/utils/searchGroupSharees.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +13351 -10855
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +348 -108
- package/l10n/en.json +56 -1
- package/l10n/nl.json +56 -1
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +545 -0
- package/src/components/CnActionDialog/index.js +12 -0
- package/src/components/CnAppNav/CnAppNav.vue +17 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +102 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +86 -7
- package/src/components/CnIndexPage/CnIndexPage.vue +409 -23
- package/src/components/CnIndexPage/useSelfFetchList.js +21 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +91 -1
- package/src/components/CnMapWidget/CnMapWidget.vue +16 -1
- package/src/components/CnPartyPicker/CnPartyPicker.vue +317 -0
- package/src/components/CnPartyPicker/index.js +11 -0
- package/src/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue +192 -0
- package/src/components/CnSavedViewEditDialog/index.js +11 -0
- package/src/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue +21 -6
- package/src/components/CnSavedViewShareFields/CnSavedViewShareFields.vue +130 -37
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +36 -599
- package/src/components/CnSavedViewsPanel/CnSavedViewsPanel.vue +459 -0
- package/src/components/CnSavedViewsPanel/index.js +11 -0
- package/src/components/index.js +4 -0
- package/src/css/index-page.css +16 -0
- package/src/css/look-board-index.css +5 -3
- package/src/css/look-board.css +18 -3
- package/src/index.js +4 -0
- package/src/mixins/savedViewsList.js +231 -0
- package/src/schemas/app-manifest-v2.schema.json +98 -3
- package/src/utils/actionsDispatcher.js +2 -2
- package/src/utils/builtInModals.js +35 -0
- package/src/utils/mapMarkerColor.js +117 -0
- package/src/utils/mergeManifestDelta.js +4 -0
- package/src/utils/savedViewHelpers.js +58 -6
- package/src/utils/savedViewMenu.js +150 -0
- package/src/utils/searchGroupSharees.js +49 -14
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -36,6 +36,24 @@ export function resolveInitialQuickFilterIndex(quickFilters) {
|
|
|
36
36
|
*/
|
|
37
37
|
export const BOARD_PAGE_SIZE = 500
|
|
38
38
|
|
|
39
|
+
/** How many rows `mapConfig.plotAll: true` plots at most. */
|
|
40
|
+
export const MAP_PLOT_ALL_LIMIT = 500
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How many rows a map that plots every matching row loads (`mapConfig.plotAll`:
|
|
44
|
+
* `true` for {@link MAP_PLOT_ALL_LIMIT}, or a number), 0 when it plots the page.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} [mapConfig] The page's map config.
|
|
47
|
+
* @return {number} The limit.
|
|
48
|
+
*/
|
|
49
|
+
export function mapPlotAllLimitOf(mapConfig) {
|
|
50
|
+
const plotAll = mapConfig && mapConfig.plotAll
|
|
51
|
+
if (plotAll === true) {
|
|
52
|
+
return MAP_PLOT_ALL_LIMIT
|
|
53
|
+
}
|
|
54
|
+
return typeof plotAll === 'number' && Number.isFinite(plotAll) && plotAll > 0 ? Math.floor(plotAll) : 0
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* The sort a quick filter sets while it is active (`sort: { key, order }`, or a
|
|
41
59
|
* list of those for a multi-column order), as sort keys, or null when it sets
|
|
@@ -267,7 +285,9 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
267
285
|
defaultSortKeys: initialSortKeys,
|
|
268
286
|
defaultActiveFilters: initialActiveFilters,
|
|
269
287
|
defaultSearchTerm: initialSearchTerm,
|
|
270
|
-
defaultPageSize: props.viewMode === 'board'
|
|
288
|
+
defaultPageSize: props.viewMode === 'board'
|
|
289
|
+
? BOARD_PAGE_SIZE
|
|
290
|
+
: ((props.viewMode === 'map' && mapPlotAllLimitOf(props.mapConfig)) || (props.pagination && props.pagination.limit) || undefined),
|
|
271
291
|
defaultVisibleColumns: configuredColumnKeys.length ? configuredColumnKeys : null,
|
|
272
292
|
// `pages[].config.extend` → OpenRegister's repeated `_extend[]`. A
|
|
273
293
|
// getter so a reactive change re-scopes the next fetch, matching how
|
|
@@ -214,6 +214,8 @@ With admin-only filter control:
|
|
|
214
214
|
| `columnsDescription` | String | `'Select which columns to display in the table'` | Description below the column visibility heading |
|
|
215
215
|
| `propertiesGroupLabel` | String | `''` | Override label for the schema properties group. Falls back to `schema.title` |
|
|
216
216
|
| `defaultTab` | String | `'search-tab'` | Tab id that is active when the sidebar opens. Built-in ids: `'search-tab'`, `'columns-tab'` |
|
|
217
|
+
| `activeTab` | String | `''` | Switch to a tab while mounted, e.g. `'filters-tab'` |
|
|
218
|
+
| `savedViews` | Object | `null` | The page's saved views, shown in a third tab, Filters (CnSavedViewsPanel); every intent comes back as one `saved-view` event `{ action, view }` |
|
|
217
219
|
| `userIsAdmin` | Boolean | `true` | When `false`, schema properties with `adminOnly: true` are hidden from filters |
|
|
218
220
|
| `filterFields` | Object | `null` | Filter declarations for a page with no schema of its own. Wins over `schema`, and feeds the Search tab only |
|
|
219
221
|
|
|
@@ -287,6 +287,32 @@
|
|
|
287
287
|
</div>
|
|
288
288
|
</NcAppSidebarTab>
|
|
289
289
|
|
|
290
|
+
<!-- Filters Tab: the page's saved views (save, list, manage, add to menu). -->
|
|
291
|
+
<NcAppSidebarTab
|
|
292
|
+
v-if="savedViews"
|
|
293
|
+
id="filters-tab"
|
|
294
|
+
:name="t('nextcloud-vue', 'Filters')"
|
|
295
|
+
:order="3">
|
|
296
|
+
<template #icon>
|
|
297
|
+
<BookmarkOutline :size="20" />
|
|
298
|
+
</template>
|
|
299
|
+
|
|
300
|
+
<div class="cn-index-sidebar__tab-content">
|
|
301
|
+
<div class="cn-index-sidebar__section">
|
|
302
|
+
<CnSavedViewsPanel
|
|
303
|
+
v-bind="savedViewsPanelProps"
|
|
304
|
+
@apply="onSavedView('apply', $event)"
|
|
305
|
+
@saveRequest="onSavedView('save', null)"
|
|
306
|
+
@editRequest="onSavedView('edit', $event)"
|
|
307
|
+
@shareRequest="onSavedView('share', $event)"
|
|
308
|
+
@updateRequest="onSavedView('update', $event)"
|
|
309
|
+
@copyRequest="onSavedView('copy', $event)"
|
|
310
|
+
@menuToggle="onSavedView('menu', $event)"
|
|
311
|
+
@deleteRequest="onSavedView('delete', $event)" />
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</NcAppSidebarTab>
|
|
315
|
+
|
|
290
316
|
<!-- Extra tabs injected by the consumer -->
|
|
291
317
|
<slot name="tabs" />
|
|
292
318
|
</NcAppSidebar>
|
|
@@ -295,6 +321,7 @@
|
|
|
295
321
|
<script>
|
|
296
322
|
import { translate as t } from '@nextcloud/l10n'
|
|
297
323
|
import { NcAppSidebar, NcAppSidebarTab, NcButton, NcCheckboxRadioSwitch, NcPopover, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
324
|
+
import BookmarkOutline from 'vue-material-design-icons/BookmarkOutline.vue'
|
|
298
325
|
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
299
326
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
300
327
|
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
@@ -305,6 +332,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
|
|
|
305
332
|
import Pin from 'vue-material-design-icons/Pin.vue'
|
|
306
333
|
import PinOutline from 'vue-material-design-icons/PinOutline.vue'
|
|
307
334
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
335
|
+
import CnSavedViewsPanel from '../CnSavedViewsPanel/CnSavedViewsPanel.vue'
|
|
308
336
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
309
337
|
import { facetOptions } from '../../utils/facets.js'
|
|
310
338
|
import { labelLang } from '../../utils/manifestTranslate.js'
|
|
@@ -332,6 +360,8 @@ export default {
|
|
|
332
360
|
components: {
|
|
333
361
|
NcAppSidebar,
|
|
334
362
|
NcAppSidebarTab,
|
|
363
|
+
CnSavedViewsPanel,
|
|
364
|
+
BookmarkOutline,
|
|
335
365
|
NcTextField,
|
|
336
366
|
NcSelect,
|
|
337
367
|
CnDateRangePicker,
|
|
@@ -551,6 +581,26 @@ export default {
|
|
|
551
581
|
default: 'search-tab',
|
|
552
582
|
},
|
|
553
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Switch to a tab while the sidebar is mounted, e.g. `'filters-tab'`
|
|
586
|
+
* after a view was saved from the toolbar. Empty leaves the tab alone.
|
|
587
|
+
*/
|
|
588
|
+
activeTab: {
|
|
589
|
+
type: String,
|
|
590
|
+
default: '',
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The page's saved views, shown in the Filters tab:
|
|
595
|
+
* `{ views, loading, currentUserId, counts, selectedViewId, maxDepth, menuIds }`
|
|
596
|
+
* as `CnSavedViewsPanel` takes them. `null` shows no saved views.
|
|
597
|
+
* Every intent comes back as one `saved-view` event.
|
|
598
|
+
*/
|
|
599
|
+
savedViews: {
|
|
600
|
+
type: Object,
|
|
601
|
+
default: null,
|
|
602
|
+
},
|
|
603
|
+
|
|
554
604
|
/**
|
|
555
605
|
* Whether the current user is an admin.
|
|
556
606
|
* When false, schema properties with `adminOnly: true` are hidden from filters.
|
|
@@ -561,7 +611,7 @@ export default {
|
|
|
561
611
|
},
|
|
562
612
|
},
|
|
563
613
|
|
|
564
|
-
emits: ['clear-filters', 'columns-change', 'columns-reorder', 'columns-reset', 'pin-change', 'filter-change', 'search', 'sort-change', 'tab-change', 'update:open'],
|
|
614
|
+
emits: ['clear-filters', 'columns-change', 'columns-reorder', 'columns-reset', 'pin-change', 'filter-change', 'saved-view', 'search', 'sort-change', 'tab-change', 'update:open'],
|
|
565
615
|
|
|
566
616
|
data() {
|
|
567
617
|
return {
|
|
@@ -638,6 +688,25 @@ export default {
|
|
|
638
688
|
return t('nextcloud-vue', 'Current order: {order}', { order })
|
|
639
689
|
},
|
|
640
690
|
|
|
691
|
+
/**
|
|
692
|
+
* The panel's props, from `savedViews`.
|
|
693
|
+
*
|
|
694
|
+
* @return {object}
|
|
695
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-6
|
|
696
|
+
*/
|
|
697
|
+
savedViewsPanelProps() {
|
|
698
|
+
const f = this.savedViews || {}
|
|
699
|
+
return {
|
|
700
|
+
views: Array.isArray(f.views) ? f.views : [],
|
|
701
|
+
loading: f.loading === true,
|
|
702
|
+
currentUserId: f.currentUserId || '',
|
|
703
|
+
counts: f.counts || null,
|
|
704
|
+
selectedViewId: f.selectedViewId || '',
|
|
705
|
+
maxDepth: typeof f.maxDepth === 'number' ? f.maxDepth : 3,
|
|
706
|
+
menuIds: Array.isArray(f.menuIds) ? f.menuIds : null,
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
|
|
641
710
|
/** Resolved icon — explicit prop overrides schema.icon */
|
|
642
711
|
resolvedIcon() {
|
|
643
712
|
return this.icon || this.schema?.icon || ''
|
|
@@ -760,6 +829,12 @@ export default {
|
|
|
760
829
|
},
|
|
761
830
|
|
|
762
831
|
watch: {
|
|
832
|
+
activeTab(tab) {
|
|
833
|
+
if (tab) {
|
|
834
|
+
this.internalActiveTab = tab
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
|
|
763
838
|
open(val) {
|
|
764
839
|
this.internalOpen = val
|
|
765
840
|
},
|
|
@@ -840,6 +915,21 @@ export default {
|
|
|
840
915
|
this.$emit('sort-change', direction === 'desc' ? reverseSortKeys(keys) : keys.map((k) => ({ key: k.key, order: k.order || 'asc' })))
|
|
841
916
|
},
|
|
842
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Hand a saved-view intent to the page.
|
|
920
|
+
*
|
|
921
|
+
* @param {'apply'|'save'|'edit'|'share'|'presentation'|'update'|'copy'|'menu'|'delete'} action The intent.
|
|
922
|
+
* @param {object|null} view The view, or null for save.
|
|
923
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-6
|
|
924
|
+
*/
|
|
925
|
+
onSavedView(action, view) {
|
|
926
|
+
/**
|
|
927
|
+
* @event saved-view A saved-view intent from the Filters tab. Payload: `{ action, view }`.
|
|
928
|
+
* @type {{action: string, view: (object|null)}}
|
|
929
|
+
*/
|
|
930
|
+
this.$emit('saved-view', { action, view })
|
|
931
|
+
},
|
|
932
|
+
|
|
843
933
|
/**
|
|
844
934
|
* A manifest label through the injected lookup, or as written.
|
|
845
935
|
*
|
|
@@ -871,10 +871,16 @@ export default {
|
|
|
871
871
|
const number = feature.properties && Number(feature.properties.markerNumber)
|
|
872
872
|
if (Number.isInteger(number) && number > 0) {
|
|
873
873
|
const active = feature.properties.markerActive === true
|
|
874
|
+
// A colour of the row's own (`markerColor`) fills the pin, except
|
|
875
|
+
// the active one, which keeps the active colour.
|
|
876
|
+
const fill = feature.properties.markerColor
|
|
877
|
+
const style = !active && typeof fill === 'string' && /^[#a-zA-Z0-9(),.%\s_-]+$/.test(fill)
|
|
878
|
+
? ` class="cn-map-widget__pin-fill" style="background:${fill}"`
|
|
879
|
+
: ''
|
|
874
880
|
return L.marker(latlng, {
|
|
875
881
|
icon: L.divIcon({
|
|
876
882
|
className: active ? 'cn-map-widget__pin cn-map-widget__pin--active' : 'cn-map-widget__pin',
|
|
877
|
-
html: `<span>${number}</span>`,
|
|
883
|
+
html: `<span${style}>${number}</span>`,
|
|
878
884
|
iconSize: [26, 26],
|
|
879
885
|
iconAnchor: [13, 13],
|
|
880
886
|
}),
|
|
@@ -1512,6 +1518,15 @@ export default {
|
|
|
1512
1518
|
box-sizing: border-box;
|
|
1513
1519
|
}
|
|
1514
1520
|
|
|
1521
|
+
.cn-map-widget__pin-fill {
|
|
1522
|
+
display: flex;
|
|
1523
|
+
align-items: center;
|
|
1524
|
+
justify-content: center;
|
|
1525
|
+
width: 100%;
|
|
1526
|
+
height: 100%;
|
|
1527
|
+
border-radius: 50%;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1515
1530
|
.cn-map-widget__pin--active {
|
|
1516
1531
|
background: var(--color-primary-element-light-text);
|
|
1517
1532
|
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<CnSearchSelect
|
|
7
|
+
class="cn-party-picker"
|
|
8
|
+
:modelValue="selectedOption"
|
|
9
|
+
:options="options"
|
|
10
|
+
:inputLabel="inputLabel"
|
|
11
|
+
:labelOutside="labelOutside"
|
|
12
|
+
:placeholder="placeholder ? cnTranslate(placeholder) : ''"
|
|
13
|
+
:loading="searching"
|
|
14
|
+
:filterable="false"
|
|
15
|
+
:clearable="clearable"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
data-testid="cn-party-picker"
|
|
18
|
+
@search="onSearch"
|
|
19
|
+
@update:modelValue="onPick" />
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import axios from '@nextcloud/axios'
|
|
24
|
+
import { generateUrl } from '@nextcloud/router'
|
|
25
|
+
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
26
|
+
import { CnSearchSelect } from '../CnSearchSelect/index.js'
|
|
27
|
+
|
|
28
|
+
/** How long typing rests before a search runs, in ms. */
|
|
29
|
+
const SEARCH_DELAY = 250
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CnPartyPicker — one search field over several kinds of party: people and
|
|
33
|
+
* organisations from OpenRegister register sets (BRP persons, KvK companies,
|
|
34
|
+
* an app's own clients) and the reader's Nextcloud contacts, as rows with
|
|
35
|
+
* initials, the name and a second line, in one list (board DqNieuweZaak
|
|
36
|
+
* "Aanvrager": "Henk Visser · Inwoner · Beukenlaan 3", "Visser Bouw B.V. ·
|
|
37
|
+
* Organisatie · KvK …"). Built on CnSearchSelect.
|
|
38
|
+
*
|
|
39
|
+
* `sources`: each `{ id, register, schema, label?, title?, secondary?,
|
|
40
|
+
* limit?, icon? }` searches OpenRegister with `_search`; `title` and
|
|
41
|
+
* `secondary` are templates filled from the row (default `{name}` / none),
|
|
42
|
+
* `label` names the kind ("Resident") and starts the second line. A source
|
|
43
|
+
* `{ id, type: "contacts", label? }` searches the reader's Nextcloud contacts.
|
|
44
|
+
*
|
|
45
|
+
* `update:modelValue` emits the chosen row's id (a contact has none and
|
|
46
|
+
* emits its contact id); `select` emits the whole choice: `{ source, id,
|
|
47
|
+
* label, secondary, row }`, so a host can store more than the id (a case's
|
|
48
|
+
* initiator type, source id and display name). A `modelValue` the list does
|
|
49
|
+
* not hold is shown by its `selectedLabel`, else looked up by id in the
|
|
50
|
+
* OpenRegister sources.
|
|
51
|
+
*
|
|
52
|
+
* ```vue
|
|
53
|
+
* <CnPartyPicker
|
|
54
|
+
* v-model="case.requester"
|
|
55
|
+
* input-label="Requester"
|
|
56
|
+
* :sources="[
|
|
57
|
+
* { id: 'person', register: 'dossiq', schema: 'brpPerson', label: 'Resident', title: '{firstName} {lastName}', secondary: '{residence.street} {residence.houseNumber}' },
|
|
58
|
+
* { id: 'company', register: 'dossiq', schema: 'kvkCompany', label: 'Organisation', title: '{tradeName}', secondary: 'KvK {sourceId}' },
|
|
59
|
+
* { id: 'contact', type: 'contacts', label: 'Contact' },
|
|
60
|
+
* ]"
|
|
61
|
+
* @select="onRequester" />
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export default {
|
|
65
|
+
name: 'CnPartyPicker',
|
|
66
|
+
|
|
67
|
+
components: { CnSearchSelect },
|
|
68
|
+
|
|
69
|
+
inject: {
|
|
70
|
+
/** The host app's translate (CnAppRoot `cnTranslate`). */
|
|
71
|
+
cnTranslate: { default: () => (key) => key },
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
props: {
|
|
75
|
+
/** The chosen party's id. */
|
|
76
|
+
modelValue: {
|
|
77
|
+
type: [String, Number],
|
|
78
|
+
default: null,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where to search: OpenRegister sets `{ id, register, schema, label?,
|
|
83
|
+
* title?, secondary?, limit?, icon? }`, or `{ id, type: "contacts",
|
|
84
|
+
* label? }` for the reader's Nextcloud contacts.
|
|
85
|
+
*
|
|
86
|
+
* @type {Array<object>}
|
|
87
|
+
*/
|
|
88
|
+
sources: {
|
|
89
|
+
type: Array,
|
|
90
|
+
default: () => [],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/** The field's label. */
|
|
94
|
+
inputLabel: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: '',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/** Draw the label above the field. */
|
|
100
|
+
labelOutside: {
|
|
101
|
+
type: Boolean,
|
|
102
|
+
default: false,
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/** Placeholder ("Name, BSN, trade name or KvK number"), through the host translate. */
|
|
106
|
+
placeholder: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: '',
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/** The chosen party's name, shown before the list holds it. */
|
|
112
|
+
selectedLabel: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: '',
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/** Offer a clear button. */
|
|
118
|
+
clearable: {
|
|
119
|
+
type: Boolean,
|
|
120
|
+
default: true,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/** Disable the field. */
|
|
124
|
+
disabled: {
|
|
125
|
+
type: Boolean,
|
|
126
|
+
default: false,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/** The fewest characters a search needs. */
|
|
130
|
+
minLength: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 2,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
emits: ['update:modelValue', 'select'],
|
|
137
|
+
|
|
138
|
+
data() {
|
|
139
|
+
return {
|
|
140
|
+
options: [],
|
|
141
|
+
searching: false,
|
|
142
|
+
known: null,
|
|
143
|
+
timer: null,
|
|
144
|
+
searchSeq: 0,
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
computed: {
|
|
149
|
+
/**
|
|
150
|
+
* The option for `modelValue`: in the list, else the known choice,
|
|
151
|
+
* else one named by `selectedLabel`.
|
|
152
|
+
*
|
|
153
|
+
* @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party
|
|
154
|
+
* @return {object|null}
|
|
155
|
+
*/
|
|
156
|
+
selectedOption() {
|
|
157
|
+
const value = this.modelValue
|
|
158
|
+
if (value === null || value === undefined || value === '') {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
const found = this.options.find((o) => String(o.id) === String(value))
|
|
162
|
+
if (found) {
|
|
163
|
+
return found
|
|
164
|
+
}
|
|
165
|
+
if (this.known && String(this.known.id) === String(value)) {
|
|
166
|
+
return this.known
|
|
167
|
+
}
|
|
168
|
+
return { id: value, label: this.selectedLabel || String(value), avatar: true }
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
watch: {
|
|
173
|
+
modelValue: {
|
|
174
|
+
immediate: true,
|
|
175
|
+
handler(value) {
|
|
176
|
+
if (value && !this.selectedLabel && !this.options.some((o) => String(o.id) === String(value))) {
|
|
177
|
+
this.lookUp(value)
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
beforeUnmount() {
|
|
184
|
+
clearTimeout(this.timer)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
methods: {
|
|
188
|
+
/**
|
|
189
|
+
* A source's row as a picker option.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} source The source.
|
|
192
|
+
* @param {object} row The row.
|
|
193
|
+
* @return {object|null} The option, or null without an id.
|
|
194
|
+
*/
|
|
195
|
+
optionOf(source, row) {
|
|
196
|
+
const id = row && (row.id ?? row['@self']?.id ?? row.uuid)
|
|
197
|
+
if (id === undefined || id === null || id === '') {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
const title = (source.title ? fillTemplate(source.title, row) : (row.name || row.title || row.displayName || '')).trim() || String(id)
|
|
201
|
+
const detail = source.secondary ? fillTemplate(source.secondary, row).trim() : ''
|
|
202
|
+
const kind = source.label ? this.cnTranslate(source.label) : ''
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
label: title,
|
|
206
|
+
secondary: [kind, detail].filter(Boolean).join(' · '),
|
|
207
|
+
avatar: !source.icon,
|
|
208
|
+
icon: source.icon || '',
|
|
209
|
+
source: source.id,
|
|
210
|
+
row,
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A Nextcloud contact as a picker option.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} source The contacts source.
|
|
218
|
+
* @param {object} contact The contactsmenu entry.
|
|
219
|
+
* @return {object|null}
|
|
220
|
+
*/
|
|
221
|
+
contactOptionOf(source, contact) {
|
|
222
|
+
const id = contact && (contact.id || contact.uid)
|
|
223
|
+
if (!id) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
const mail = Array.isArray(contact.emailAddresses) ? contact.emailAddresses[0] : ''
|
|
227
|
+
const kind = source.label ? this.cnTranslate(source.label) : ''
|
|
228
|
+
return { id, label: contact.fullName || String(id), secondary: [kind, mail].filter(Boolean).join(' · '), avatar: true, source: source.id, row: contact }
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Search after typing rests.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} query The typed text.
|
|
235
|
+
*/
|
|
236
|
+
onSearch(query) {
|
|
237
|
+
clearTimeout(this.timer)
|
|
238
|
+
const term = String(query || '').trim()
|
|
239
|
+
if (term.length < this.minLength) {
|
|
240
|
+
this.options = this.selectedOption ? [this.selectedOption] : []
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
this.timer = setTimeout(() => this.search(term), SEARCH_DELAY)
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Search every source at once; a source that fails adds nothing.
|
|
248
|
+
*
|
|
249
|
+
* @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party
|
|
250
|
+
* @param {string} term The search term.
|
|
251
|
+
* @return {Promise<void>}
|
|
252
|
+
*/
|
|
253
|
+
async search(term) {
|
|
254
|
+
const seq = ++this.searchSeq
|
|
255
|
+
this.searching = true
|
|
256
|
+
const sets = await Promise.all((this.sources || []).map(async (source) => {
|
|
257
|
+
try {
|
|
258
|
+
if (source.type === 'contacts') {
|
|
259
|
+
const { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })
|
|
260
|
+
return (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))
|
|
261
|
+
}
|
|
262
|
+
if (!source.register || !source.schema) {
|
|
263
|
+
return []
|
|
264
|
+
}
|
|
265
|
+
const { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })
|
|
266
|
+
return (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))
|
|
267
|
+
} catch {
|
|
268
|
+
return []
|
|
269
|
+
}
|
|
270
|
+
}))
|
|
271
|
+
if (seq !== this.searchSeq) {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
this.options = sets.flat().filter(Boolean)
|
|
275
|
+
this.searching = false
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Name a value the list does not hold: the first OpenRegister source
|
|
280
|
+
* that has an object with that id.
|
|
281
|
+
*
|
|
282
|
+
* @param {string|number} id The id.
|
|
283
|
+
* @return {Promise<void>}
|
|
284
|
+
*/
|
|
285
|
+
async lookUp(id) {
|
|
286
|
+
for (const source of (this.sources || []).filter((s) => s.register && s.schema && s.type !== 'contacts')) {
|
|
287
|
+
try {
|
|
288
|
+
const { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}/${encodeURIComponent(id)}`))
|
|
289
|
+
const option = data ? this.optionOf(source, data) : null
|
|
290
|
+
if (option && String(this.modelValue) === String(id)) {
|
|
291
|
+
this.known = option
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Not in this source; try the next.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Take the picked option: emit its id and the whole choice.
|
|
302
|
+
*
|
|
303
|
+
* @param {string|number|null} id The picked id.
|
|
304
|
+
*/
|
|
305
|
+
onPick(id) {
|
|
306
|
+
const option = id === null ? null : this.options.find((o) => String(o.id) === String(id)) || null
|
|
307
|
+
if (option) {
|
|
308
|
+
this.known = option
|
|
309
|
+
}
|
|
310
|
+
/** @event update:modelValue The chosen party's id, or null when cleared. */
|
|
311
|
+
this.$emit('update:modelValue', id)
|
|
312
|
+
/** @event select The whole choice: `{ source, id, label, secondary, row }`, or null when cleared. */
|
|
313
|
+
this.$emit('select', option ? { source: option.source, id: option.id, label: option.label, secondary: option.secondary, row: option.row } : null)
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnPartyPicker — one search field over people, organisations and contacts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import CnPartyPicker from './CnPartyPicker.vue'
|
|
9
|
+
|
|
10
|
+
export { CnPartyPicker }
|
|
11
|
+
export default CnPartyPicker
|