@conduction/nextcloud-vue 2.141.0 → 2.143.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.
Files changed (140) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
  10. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
  12. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -0
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  27. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  28. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  31. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +21 -0
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +70 -5
  47. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +5 -3
  49. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  51. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  52. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  53. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +36 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  59. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  66. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  67. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  84. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  85. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  86. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  87. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  88. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  89. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  90. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  91. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  92. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  93. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  94. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +10 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +12 -3
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +976 -592
  101. package/dist/esm/utils/buildViewTree.js +23 -8
  102. package/dist/esm/utils/buildViewTree.js.map +1 -1
  103. package/dist/nextcloud-vue.cjs.js +1363 -734
  104. package/dist/nextcloud-vue.cjs.js.map +2 -2
  105. package/dist/nextcloud-vue.css +976 -592
  106. package/l10n/en.json +10 -1
  107. package/l10n/nl.json +12 -3
  108. package/package.json +1 -1
  109. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  110. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  111. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  112. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  113. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  114. package/src/components/CnCellRenderer/CnCellRenderer.vue +15 -1
  115. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  116. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +22 -1
  119. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  120. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  121. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +72 -7
  122. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  123. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  124. package/src/components/CnIndexPage/CnIndexPage.vue +37 -2
  125. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  127. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  128. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  129. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  130. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  131. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  132. package/src/css/dialog.css +18 -0
  133. package/src/css/look-board-detail.css +126 -0
  134. package/src/css/look-board-index.css +95 -1
  135. package/src/css/look-board.css +90 -1
  136. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  137. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  138. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  139. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  140. package/src/utils/buildViewTree.js +23 -8
@@ -1 +1 @@
1
- {"version":3,"file":"CnPartyPicker.vue3.js","sources":["../../../../src/components/CnPartyPicker/CnPartyPicker.vue?vue&type=template&id=44764194&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-party-picker\">\n\t\t<CnSearchSelect\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"options\"\n\t\t\t:inputLabel=\"inputLabel\"\n\t\t\t:labelOutside=\"labelOutside\"\n\t\t\t:placeholder=\"placeholder ? cnTranslate(placeholder) : ''\"\n\t\t\t:loading=\"searching\"\n\t\t\t:filterable=\"false\"\n\t\t\t:clearable=\"clearable\"\n\t\t\t:disabled=\"disabled\"\n\t\t\tdata-testid=\"cn-party-picker\"\n\t\t\t@search=\"onSearch\"\n\t\t\t@update:modelValue=\"onPick\" />\n\t\t<p v-if=\"refusal\"\n\t\t\tclass=\"cn-party-picker__refusal\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-party-picker-refusal\">\n\t\t\t{{ t('nextcloud-vue', 'This search could not be read.') }} {{ refusal }}\n\t\t</p>\n\t\t<p v-for=\"name in unavailable\"\n\t\t\t:key=\"name\"\n\t\t\tclass=\"cn-party-picker__unavailable\"\n\t\t\trole=\"status\"\n\t\t\tdata-testid=\"cn-party-picker-unavailable\">\n\t\t\t{{ t('nextcloud-vue', '{source} could not be searched, so this list may be incomplete.', { source: name }) }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnSearchSelect } from '../CnSearchSelect/index.js'\n\n/** How long typing rests before a search runs, in ms. */\nconst SEARCH_DELAY = 250\n\n/**\n * CnPartyPicker — one search field over several kinds of party: people and\n * organisations from OpenRegister register sets (BRP persons, KvK companies,\n * an app's own clients) and the reader's Nextcloud contacts, as rows with\n * initials, the name and a second line, in one list (board DqNieuweZaak\n * \"Aanvrager\": \"Henk Visser · Inwoner · Beukenlaan 3\", \"Visser Bouw B.V. ·\n * Organisatie · KvK …\"). Built on CnSearchSelect.\n *\n * `sources`: each `{ id, register, schema, label?, title?, secondary?,\n * limit?, icon? }` searches OpenRegister with `_search`; `title` and\n * `secondary` are templates filled from the row (default `{name}` / none),\n * `label` names the kind (\"Resident\") and starts the second line; `label`\n * and `secondary` go through the host translate, so a manifest can write\n * `\"CoC {kvkNumber}\"` and a Dutch reader sees \"KvK 24157431\". A source\n * `{ id, type: \"contacts\", label? }` searches the reader's Nextcloud contacts.\n *\n * `update:modelValue` emits the chosen row's id (a contact has none and\n * emits its contact id); `select` emits the whole choice: `{ source, id,\n * label, secondary, row }`, so a host can store more than the id (a case's\n * initiator type, source id and display name). A `modelValue` the list does\n * not hold is shown by its `selectedLabel`, else looked up by id in the\n * OpenRegister sources.\n *\n * ```vue\n * <CnPartyPicker\n * v-model=\"case.requester\"\n * input-label=\"Requester\"\n * :sources=\"[\n * { id: 'person', register: 'dossiq', schema: 'brpPerson', label: 'Resident', title: '{firstName} {lastName}', secondary: '{residence.street} {residence.houseNumber}' },\n * { id: 'company', register: 'dossiq', schema: 'kvkCompany', label: 'Organisation', title: '{tradeName}', secondary: 'KvK {sourceId}' },\n * { id: 'contact', type: 'contacts', label: 'Contact' },\n * ]\"\n * @select=\"onRequester\" />\n * ```\n */\nexport default {\n\tname: 'CnPartyPicker',\n\n\tcomponents: { CnSearchSelect },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The chosen party's id. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to search: OpenRegister sets `{ id, register, schema, label?,\n\t\t * title?, secondary?, limit?, icon? }`, or `{ id, type: \"contacts\",\n\t\t * label? }` for the reader's Nextcloud contacts.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The field's label. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Draw the label above the field. */\n\t\tlabelOutside: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder (\"Name, BSN, trade name or KvK number\"), through the host translate. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The chosen party's name, shown before the list holds it. */\n\t\tselectedLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Offer a clear button. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Disable the field. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The fewest characters a search needs. */\n\t\tminLength: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'select'],\n\n\tdata() {\n\t\treturn {\n\t\t\toptions: [],\n\t\t\tsearching: false,\n\t\t\tknown: null,\n\t\t\ttimer: null,\n\t\t\tsearchSeq: 0,\n\t\t\trefusal: '',\n\t\t\tunavailable: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The option for `modelValue`: in the list, else the known choice,\n\t\t * else one named by `selectedLabel`.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @return {object|null}\n\t\t */\n\t\tselectedOption() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst found = this.options.find((o) => String(o.id) === String(value))\n\t\t\tif (found) {\n\t\t\t\treturn found\n\t\t\t}\n\t\t\tif (this.known && String(this.known.id) === String(value)) {\n\t\t\t\treturn this.known\n\t\t\t}\n\t\t\treturn { id: value, label: this.selectedLabel || String(value), avatar: true }\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tif (value && !this.selectedLabel && !this.options.some((o) => String(o.id) === String(value))) {\n\t\t\t\t\tthis.lookUp(value)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.timer)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A source's row as a picker option.\n\t\t *\n\t\t * @param {object} source The source.\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The option, or null without an id.\n\t\t */\n\t\toptionOf(source, row) {\n\t\t\tconst id = row && (row.id ?? row['@self']?.id ?? row.uuid)\n\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst title = (source.title ? fillTemplate(source.title, row) : (row.name || row.title || row.displayName || '')).trim() || String(id)\n\t\t\tconst detail = source.secondary ? fillTemplate(this.cnTranslate(source.secondary), row).trim() : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tlabel: title,\n\t\t\t\tsecondary: [kind, detail].filter(Boolean).join(' · '),\n\t\t\t\tavatar: !source.icon,\n\t\t\t\ticon: source.icon || '',\n\t\t\t\tsource: source.id,\n\t\t\t\trow,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A Nextcloud contact as a picker option.\n\t\t *\n\t\t * @param {object} source The contacts source.\n\t\t * @param {object} contact The contactsmenu entry.\n\t\t * @return {object|null}\n\t\t */\n\t\tcontactOptionOf(source, contact) {\n\t\t\tconst id = contact && (contact.id || contact.uid)\n\t\t\tif (!id) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst mail = Array.isArray(contact.emailAddresses) ? contact.emailAddresses[0] : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn { id, label: contact.fullName || String(id), secondary: [kind, mail].filter(Boolean).join(' · '), avatar: true, source: source.id, row: contact }\n\t\t},\n\n\t\t/**\n\t\t * Search after typing rests.\n\t\t *\n\t\t * @param {string} query The typed text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tconst term = String(query || '').trim()\n\t\t\tif (term.length < this.minLength) {\n\t\t\t\tthis.options = this.selectedOption ? [this.selectedOption] : []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.timer = setTimeout(() => this.search(term), SEARCH_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Search every source at once. A term a source refused (400) is said\n\t\t * under the field in its words; a source that could not be searched at\n\t\t * all is named, so an empty list never hides a missing source.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @param {string} term The search term.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search(term) {\n\t\t\tconst seq = ++this.searchSeq\n\t\t\tthis.searching = true\n\t\t\tlet refusal = ''\n\t\t\tconst unavailable = []\n\t\t\tconst sets = await Promise.all((this.sources || []).map(async (source) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (source.type === 'contacts') {\n\t\t\t\t\t\tconst { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })\n\t\t\t\t\t\treturn (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))\n\t\t\t\t\t}\n\t\t\t\t\tif (!source.register || !source.schema) {\n\t\t\t\t\t\treturn []\n\t\t\t\t\t}\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })\n\t\t\t\t\treturn (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))\n\t\t\t\t} catch (failure) {\n\t\t\t\t\t// OpenRegister refuses a term it cannot parse with a 400 rather\n\t\t\t\t\t// than answering nothing, so that refusal is said, not hidden.\n\t\t\t\t\tconst response = failure && failure.response\n\t\t\t\t\tif (response && response.status === 400) {\n\t\t\t\t\t\tconst body = response.data\n\t\t\t\t\t\trefusal = String((body && (body.message || body.error)) || (typeof body === 'string' ? body : '') || refusal)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunavailable.push(source.label ? this.cnTranslate(source.label) : String(source.id || ''))\n\t\t\t\t\t}\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq !== this.searchSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.options = sets.flat().filter(Boolean)\n\t\t\tthis.refusal = refusal\n\t\t\tthis.unavailable = unavailable.filter(Boolean)\n\t\t\tthis.searching = false\n\t\t},\n\n\t\t/**\n\t\t * Name a value the list does not hold: the first OpenRegister source\n\t\t * that has an object with that id.\n\t\t *\n\t\t * @param {string|number} id The id.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync lookUp(id) {\n\t\t\tfor (const source of (this.sources || []).filter((s) => s.register && s.schema && s.type !== 'contacts')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}/${encodeURIComponent(id)}`))\n\t\t\t\t\tconst option = data ? this.optionOf(source, data) : null\n\t\t\t\t\tif (option && String(this.modelValue) === String(id)) {\n\t\t\t\t\t\tthis.known = option\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not in this source; try the next.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Take the picked option: emit its id and the whole choice.\n\t\t *\n\t\t * @param {string|number|null} id The picked id.\n\t\t */\n\t\tonPick(id) {\n\t\t\tconst option = id === null ? null : this.options.find((o) => String(o.id) === String(id)) || null\n\t\t\tif (option) {\n\t\t\t\tthis.known = option\n\t\t\t}\n\t\t\t/** @event update:modelValue The chosen party's id, or null when cleared. */\n\t\t\tthis.$emit('update:modelValue', id)\n\t\t\t/** @event select The whole choice: `{ source, id, label, secondary, row }`, or null when cleared. */\n\t\t\tthis.$emit('select', option ? { source: option.source, id: option.id, label: option.label, secondary: option.secondary, row: option.row } : null)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-party-picker__refusal {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-party-picker__unavailable {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_toDisplayString","_createElementBlock"],"mappings":";;AASM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,iBAAc,GAAA;;;AAevB,EAAA,KAAE,EAAA,0BAAA;AACF,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAU,EAAA,yBAAA;;;;;;;eAJoB,CAAA,yBAAA,EAAA;AAX7B,MAAA,UAAU,EAAE,QAAa,CAAA,cAAA;AACzB,MAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,MAAA,UAAU,EAAE,MAAK,CAAA,UAAA;AACjB,MAAA,YAAY,EAAE,MAAM,CAAA,YAAA;AACpB,MAAA,WAAW,EAAE,MAAA,CAAA,WAAK,GAAA,QAAA,CAAA,WAAA,CAAA,MAAA,CAAA,WAAA,CAAA,GAAA,EAAA;AAClB,MAAA,OAAO,EAAE,KAAS,CAAA,SAAA;AAClB,MAAA,UAAU,EAAE,KAAI;AAChB,MAAA,SAAS,EAAE,MAAS,CAAA,SAAA;AACnB,MAAA,QAAO,EAAE,MAAI,CAAA,QAAA;AACf,MAAA,aAAW,EAAC,iBAAiB;AAC5B,MAAA,QAAM,EAAE,QAAE,CAAA,QAAA;AACV,MAAA,qBAAiB,EAAE,QAAM,CAAA,MAAA;;KAClB,KAAO,CAAA,OAAA;yCAKZ,eAAA,EAAAA,eAAA,CADA,UAAC,CAA+B,eAAA,EAAA,gCAAA,CAAA,CAAA,GAAA,GAAA,mBAAA,KAAA,CAAA,OAAA,CAAA,EAAA,CAAA,YAAA;;sBAEnCC,8CAAiB,KAAW,CAAA,WAAA,EAAA,CAAnB,IAAI,KAAA;2BAAbA;AACC,QAAA,GAAG,EAAE,IAAI;AACV,QAAA,KAAE,EAAA,8BAAA;AACF,QAAA,IAAG,EAAA,QAAA;;;;;;;;;"}
1
+ {"version":3,"file":"CnPartyPicker.vue3.js","sources":["../../../../src/components/CnPartyPicker/CnPartyPicker.vue?vue&type=template&id=1b4d732c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-party-picker\">\n\t\t<CnSearchSelect\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"options\"\n\t\t\t:inputLabel=\"inputLabel\"\n\t\t\t:labelOutside=\"labelOutside\"\n\t\t\t:placeholder=\"placeholder ? cnTranslate(placeholder) : ''\"\n\t\t\t:loading=\"searching\"\n\t\t\t:filterable=\"false\"\n\t\t\t:clearable=\"clearable\"\n\t\t\t:disabled=\"disabled\"\n\t\t\tdata-testid=\"cn-party-picker\"\n\t\t\t@search=\"onSearch\"\n\t\t\t@update:modelValue=\"onPick\" />\n\t\t<p v-if=\"refusal\"\n\t\t\tclass=\"cn-party-picker__refusal\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-party-picker-refusal\">\n\t\t\t{{ t('nextcloud-vue', 'This search could not be read.') }} {{ refusal }}\n\t\t</p>\n\t\t<p v-for=\"name in unavailable\"\n\t\t\t:key=\"name\"\n\t\t\tclass=\"cn-party-picker__unavailable\"\n\t\t\trole=\"status\"\n\t\t\tdata-testid=\"cn-party-picker-unavailable\">\n\t\t\t{{ t('nextcloud-vue', '{source} could not be searched, so this list may be incomplete.', { source: name }) }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnSearchSelect } from '../CnSearchSelect/index.js'\n\n/** How long typing rests before a search runs, in ms. */\nconst SEARCH_DELAY = 250\n\n/**\n * CnPartyPicker — one search field over several kinds of party: people and\n * organisations from OpenRegister register sets (BRP persons, KvK companies,\n * an app's own clients) and the reader's Nextcloud contacts, as rows with\n * initials, the name and a second line, in one list (board DqNieuweZaak\n * \"Aanvrager\": \"Henk Visser · Inwoner · Beukenlaan 3\", \"Visser Bouw B.V. ·\n * Organisatie · KvK …\"). Built on CnSearchSelect.\n *\n * `sources`: each `{ id, register, schema, label?, title?, secondary?,\n * limit?, icon?, lookup? }` searches OpenRegister with `_search`; `lookup`\n * (`{ url, when }`) sends a term matching `when` to the app's exact lookup\n * instead (a POST of `{ term }` answering `{ results }`); `title` and\n * `secondary` are templates filled from the row (default `{name}` / none),\n * `label` names the kind (\"Resident\") and starts the second line; `label`\n * and `secondary` go through the host translate, so a manifest can write\n * `\"CoC {kvkNumber}\"` and a Dutch reader sees \"KvK 24157431\". A source\n * `{ id, type: \"contacts\", label? }` searches the reader's Nextcloud contacts.\n *\n * `update:modelValue` emits the chosen row's id (a contact has none and\n * emits its contact id); `select` emits the whole choice: `{ source, id,\n * label, secondary, row }`, so a host can store more than the id (a case's\n * initiator type, source id and display name). A `modelValue` the list does\n * not hold is shown by its `selectedLabel`, else looked up by id in the\n * OpenRegister sources.\n *\n * ```vue\n * <CnPartyPicker\n * v-model=\"case.requester\"\n * input-label=\"Requester\"\n * :sources=\"[\n * { id: 'person', register: 'dossiq', schema: 'brpPerson', label: 'Resident', title: '{firstName} {lastName}', secondary: '{residence.street} {residence.houseNumber}' },\n * { id: 'company', register: 'dossiq', schema: 'kvkCompany', label: 'Organisation', title: '{tradeName}', secondary: 'KvK {sourceId}' },\n * { id: 'contact', type: 'contacts', label: 'Contact' },\n * ]\"\n * @select=\"onRequester\" />\n * ```\n */\nexport default {\n\tname: 'CnPartyPicker',\n\n\tcomponents: { CnSearchSelect },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The chosen party's id. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to search: OpenRegister sets `{ id, register, schema, label?,\n\t\t * title?, secondary?, limit?, icon? }`, or `{ id, type: \"contacts\",\n\t\t * label? }` for the reader's Nextcloud contacts.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The field's label. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Draw the label above the field. */\n\t\tlabelOutside: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder (\"Name, BSN, trade name or KvK number\"), through the host translate. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The chosen party's name, shown before the list holds it. */\n\t\tselectedLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Offer a clear button. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Disable the field. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The fewest characters a search needs. */\n\t\tminLength: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'select'],\n\n\tdata() {\n\t\treturn {\n\t\t\toptions: [],\n\t\t\tsearching: false,\n\t\t\tknown: null,\n\t\t\ttimer: null,\n\t\t\tsearchSeq: 0,\n\t\t\trefusal: '',\n\t\t\tunavailable: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The option for `modelValue`: in the list, else the known choice,\n\t\t * else one named by `selectedLabel`.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @return {object|null}\n\t\t */\n\t\tselectedOption() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst found = this.options.find((o) => String(o.id) === String(value))\n\t\t\tif (found) {\n\t\t\t\treturn found\n\t\t\t}\n\t\t\tif (this.known && String(this.known.id) === String(value)) {\n\t\t\t\treturn this.known\n\t\t\t}\n\t\t\treturn { id: value, label: this.selectedLabel || String(value), avatar: true }\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tif (value && !this.selectedLabel && !this.options.some((o) => String(o.id) === String(value))) {\n\t\t\t\t\tthis.lookUp(value)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.timer)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A source's row as a picker option.\n\t\t *\n\t\t * @param {object} source The source.\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The option, or null without an id.\n\t\t */\n\t\toptionOf(source, row) {\n\t\t\tconst id = row && (row.id ?? row['@self']?.id ?? row.uuid)\n\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst title = (source.title ? fillTemplate(source.title, row) : (row.name || row.title || row.displayName || '')).trim() || String(id)\n\t\t\tconst detail = source.secondary ? fillTemplate(this.cnTranslate(source.secondary), row).trim() : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tlabel: title,\n\t\t\t\tsecondary: [kind, detail].filter(Boolean).join(' · '),\n\t\t\t\tavatar: !source.icon,\n\t\t\t\ticon: source.icon || '',\n\t\t\t\tsource: source.id,\n\t\t\t\trow,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A Nextcloud contact as a picker option.\n\t\t *\n\t\t * @param {object} source The contacts source.\n\t\t * @param {object} contact The contactsmenu entry.\n\t\t * @return {object|null}\n\t\t */\n\t\tcontactOptionOf(source, contact) {\n\t\t\tconst id = contact && (contact.id || contact.uid)\n\t\t\tif (!id) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst mail = Array.isArray(contact.emailAddresses) ? contact.emailAddresses[0] : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn { id, label: contact.fullName || String(id), secondary: [kind, mail].filter(Boolean).join(' · '), avatar: true, source: source.id, row: contact }\n\t\t},\n\n\t\t/**\n\t\t * Search after typing rests.\n\t\t *\n\t\t * @param {string} query The typed text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tconst term = String(query || '').trim()\n\t\t\tif (term.length < this.minLength) {\n\t\t\t\tthis.options = this.selectedOption ? [this.selectedOption] : []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.timer = setTimeout(() => this.search(term), SEARCH_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Search every source at once. A term a source refused (400) is said\n\t\t * under the field in its words; a source that could not be searched at\n\t\t * all is named, so an empty list never hides a missing source.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @param {string} term The search term.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search(term) {\n\t\t\tconst seq = ++this.searchSeq\n\t\t\tthis.searching = true\n\t\t\tlet refusal = ''\n\t\t\tconst unavailable = []\n\t\t\tconst sets = await Promise.all((this.sources || []).map(async (source) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (source.type === 'contacts') {\n\t\t\t\t\t\tconst { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })\n\t\t\t\t\t\treturn (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))\n\t\t\t\t\t}\n\t\t\t\t\tif (this.isExactLookup(source, term) === true) {\n\t\t\t\t\t\tconst { data } = await axios.post(generateUrl(source.lookup.url), { term })\n\t\t\t\t\t\treturn (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))\n\t\t\t\t\t}\n\t\t\t\t\tif (!source.register || !source.schema) {\n\t\t\t\t\t\treturn []\n\t\t\t\t\t}\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })\n\t\t\t\t\treturn (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))\n\t\t\t\t} catch (failure) {\n\t\t\t\t\t// OpenRegister refuses a term it cannot parse with a 400 rather\n\t\t\t\t\t// than answering nothing, so that refusal is said, not hidden.\n\t\t\t\t\tconst response = failure && failure.response\n\t\t\t\t\tif (response && response.status === 400) {\n\t\t\t\t\t\tconst body = response.data\n\t\t\t\t\t\trefusal = String((body && (body.message || body.error)) || (typeof body === 'string' ? body : '') || refusal)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunavailable.push(source.label ? this.cnTranslate(source.label) : String(source.id || ''))\n\t\t\t\t\t}\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq !== this.searchSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.options = sets.flat().filter(Boolean)\n\t\t\tthis.refusal = refusal\n\t\t\tthis.unavailable = unavailable.filter(Boolean)\n\t\t\tthis.searching = false\n\t\t},\n\n\t\t/**\n\t\t * Whether a typed term goes to the source's exact lookup instead of the\n\t\t * free-text search: `source.lookup` is `{ url, when }`, `when` a regular\n\t\t * expression the whole term must match (\"^[0-9]{9}$\" for a BSN). The\n\t\t * lookup is a POST, so the term never sits in a URL or a server log line,\n\t\t * and the app behind it may check and record who looked whom up.\n\t\t *\n\t\t * @param {object} source The source.\n\t\t * @param {string} term The typed term.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/party-picker-exact-lookup/specs/party-picker/spec.md#requirement-a-party-source-can-look-a-term-up-exactly\n\t\t */\n\t\tisExactLookup(source, term) {\n\t\t\tconst lookup = source && source.lookup\n\t\t\tif (!lookup || typeof lookup.url !== 'string' || lookup.url === '' || typeof lookup.when !== 'string') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new RegExp(lookup.when).test(term)\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Name a value the list does not hold: the first OpenRegister source\n\t\t * that has an object with that id.\n\t\t *\n\t\t * @param {string|number} id The id.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync lookUp(id) {\n\t\t\tfor (const source of (this.sources || []).filter((s) => s.register && s.schema && s.type !== 'contacts')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}/${encodeURIComponent(id)}`))\n\t\t\t\t\tconst option = data ? this.optionOf(source, data) : null\n\t\t\t\t\tif (option && String(this.modelValue) === String(id)) {\n\t\t\t\t\t\tthis.known = option\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not in this source; try the next.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Take the picked option: emit its id and the whole choice.\n\t\t *\n\t\t * @param {string|number|null} id The picked id.\n\t\t */\n\t\tonPick(id) {\n\t\t\tconst option = id === null ? null : this.options.find((o) => String(o.id) === String(id)) || null\n\t\t\tif (option) {\n\t\t\t\tthis.known = option\n\t\t\t}\n\t\t\t/** @event update:modelValue The chosen party's id, or null when cleared. */\n\t\t\tthis.$emit('update:modelValue', id)\n\t\t\t/** @event select The whole choice: `{ source, id, label, secondary, row }`, or null when cleared. */\n\t\t\tthis.$emit('select', option ? { source: option.source, id: option.id, label: option.label, secondary: option.secondary, row: option.row } : null)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-party-picker__refusal {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-party-picker__unavailable {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_toDisplayString","_createElementBlock"],"mappings":";;AASM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,iBAAc,GAAA;;;AAevB,EAAA,KAAE,EAAA,0BAAA;AACF,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAU,EAAA,yBAAA;;;;;;;eAJoB,CAAA,yBAAA,EAAA;AAX7B,MAAA,UAAU,EAAE,QAAa,CAAA,cAAA;AACzB,MAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,MAAA,UAAU,EAAE,MAAK,CAAA,UAAA;AACjB,MAAA,YAAY,EAAE,MAAM,CAAA,YAAA;AACpB,MAAA,WAAW,EAAE,MAAA,CAAA,WAAK,GAAA,QAAA,CAAA,WAAA,CAAA,MAAA,CAAA,WAAA,CAAA,GAAA,EAAA;AAClB,MAAA,OAAO,EAAE,KAAS,CAAA,SAAA;AAClB,MAAA,UAAU,EAAE,KAAI;AAChB,MAAA,SAAS,EAAE,MAAS,CAAA,SAAA;AACnB,MAAA,QAAO,EAAE,MAAI,CAAA,QAAA;AACf,MAAA,aAAW,EAAC,iBAAiB;AAC5B,MAAA,QAAM,EAAE,QAAE,CAAA,QAAA;AACV,MAAA,qBAAiB,EAAE,QAAM,CAAA,MAAA;;KAClB,KAAO,CAAA,OAAA;yCAKZ,eAAA,EAAAA,eAAA,CADA,UAAC,CAA+B,eAAA,EAAA,gCAAA,CAAA,CAAA,GAAA,GAAA,mBAAA,KAAA,CAAA,OAAA,CAAA,EAAA,CAAA,YAAA;;sBAEnCC,8CAAiB,KAAW,CAAA,WAAA,EAAA,CAAnB,IAAI,KAAA;2BAAbA;AACC,QAAA,GAAG,EAAE,IAAI;AACV,QAAA,KAAE,EAAA,8BAAA;AACF,QAAA,IAAG,EAAA,QAAA;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnRecordListWidget.vue2.js';
2
2
  import { render } from './CnRecordListWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-2c65cbc2";
5
+ script.__scopeId = "data-v-544b79cd";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -88,7 +88,7 @@ function unwrap(value) {
88
88
  * `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
89
89
  * payload, after `responsePath`, is the array of rows, as on the object table), or `objectRows`
90
90
  * (`{ path, where?, valuePath? }`: rows the page record holds, such as one answer of a case); `avatar` puts a circle with the initials of a name
91
- * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
91
+ * template before the title (`{ text, size: 32 }` draws the 32px circle 12px from the title, as DqZaakBesluiten); what differs is the drawing: `presentation` is `rows`, `feed`
92
92
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
93
93
  * `"Termijn {deadline|date}"`). `cards` draws a framed card per row in a
94
94
  * grid, with `trailing` as the large figure (board ShBank's accounts).
@@ -284,6 +284,17 @@ var script = {
284
284
  },
285
285
 
286
286
  computed: {
287
+ /**
288
+ * Whether the avatar is the 32px circle (`avatar.size: 32`).
289
+ *
290
+ * @return {boolean}
291
+ * @spec openspec/changes/record-list-avatar-size/specs/record-list-avatar/spec.md#requirement-a-record-list-avatar-can-be-the-small-circle
292
+ */
293
+ avatarSmall() {
294
+ const avatar = this.content?.avatar;
295
+ return Boolean(avatar && typeof avatar === 'object' && Number(avatar.size) === 32)
296
+ },
297
+
287
298
  /** @return {boolean} Whether a row's pill comes before its title (`pillAt: "start"`). */
288
299
  pillFirst() {
289
300
  return this.content?.pillAt === 'start'