@conduction/nextcloud-vue 2.138.0 → 2.140.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/CnAppRoot/CnAppRoot.vue2.js +9 -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/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +2 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +52 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +23 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +106 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +30 -7
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +59 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +44 -34
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +28 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +7 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +129 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +2 -0
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +13 -2
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -3
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +12 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +23 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +43 -13
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +48 -6
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +4 -3
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +604 -407
- package/dist/esm/utils/builtInFormatters.js +23 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/myTeam.js +24 -4
- package/dist/esm/utils/myTeam.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +5 -4
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/transitionInputs.js +43 -2
- package/dist/esm/utils/transitionInputs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +932 -236
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +604 -407
- package/l10n/en.json +4 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
- package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -3
- package/src/components/CnChartWidget/CnChartWidget.md +1 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +76 -1
- package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +78 -9
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +39 -1
- package/src/components/CnFilesBrowser/filesBrowser.js +7 -1
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +131 -8
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +10 -2
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +10 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +68 -2
- package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +43 -1
- package/src/composables/indexSources.js +48 -6
- package/src/composables/useTaskInboxStore.js +4 -3
- package/src/css/dialog.css +18 -0
- package/src/css/look-board-detail.css +24 -0
- package/src/css/look-board-index.css +21 -0
- package/src/css/look-board.css +31 -2
- package/src/css/table.css +13 -0
- package/src/utils/builtInFormatters.js +22 -0
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/myTeam.js +24 -4
- package/src/utils/resolveFilterTokens.js +5 -4
- package/src/utils/transitionInputs.js +42 -1
|
@@ -162,6 +162,26 @@
|
|
|
162
162
|
</p>
|
|
163
163
|
</div>
|
|
164
164
|
|
|
165
|
+
<!-- A days input set as a date (`dateField`): today plus the days,
|
|
166
|
+
and a chosen date sets the days. -->
|
|
167
|
+
<div v-if="field.input.dateField"
|
|
168
|
+
:key="`${field.key}-date`"
|
|
169
|
+
class="cn-transition-input-fields__field cn-transition-input-fields__wide"
|
|
170
|
+
:data-testid="field.input.dateField.testid || `cn-transition-input-date-${field.key}`">
|
|
171
|
+
<label class="cn-transition-input-fields__label"
|
|
172
|
+
:for="`${inputId(field)}-date`">{{ dateFieldLabel(field) }}</label>
|
|
173
|
+
<NcTextField :id="`${inputId(field)}-date`"
|
|
174
|
+
:modelValue="field.dateValue"
|
|
175
|
+
type="date"
|
|
176
|
+
:label="dateFieldLabel(field)"
|
|
177
|
+
:labelOutside="true"
|
|
178
|
+
:disabled="disabled"
|
|
179
|
+
@update:modelValue="setValue(field.key, daysUntilIso($event))" />
|
|
180
|
+
<span v-if="field.input.dateField.note" class="cn-transition-input-fields__muted">
|
|
181
|
+
{{ field.input.dateField.note }}
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
165
185
|
<!-- What the input means once filled in, such as the day a paused
|
|
166
186
|
term comes back. Shown only once it can be worked out. -->
|
|
167
187
|
<div v-if="field.previewText"
|
|
@@ -209,7 +229,7 @@ import { NcCheckboxRadioSwitch, NcSelect, NcTextArea, NcTextField } from '@nextc
|
|
|
209
229
|
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
210
230
|
import CnTemplateSelect from '../CnTemplateSelect/CnTemplateSelect.vue'
|
|
211
231
|
import { readPath } from '../../utils/readPath.js'
|
|
212
|
-
import { applyTemplateText, isInputFilled, resolveInputFields } from '../../utils/transitionInputs.js'
|
|
232
|
+
import { applyTemplateText, daysUntilIso, isInputFilled, resolveInputFields } from '../../utils/transitionInputs.js'
|
|
213
233
|
import { CnIcon } from '../CnIcon/index.js'
|
|
214
234
|
|
|
215
235
|
let uid = 0
|
|
@@ -505,6 +525,28 @@ export default {
|
|
|
505
525
|
return this.markOptional && !field.required ? `${field.label} (${t('nextcloud-vue', 'optional')})` : field.label
|
|
506
526
|
},
|
|
507
527
|
|
|
528
|
+
/**
|
|
529
|
+
* The label of a days input's date field (`dateField`), with the
|
|
530
|
+
* optional marker when asked for: the date is never required.
|
|
531
|
+
*
|
|
532
|
+
* @param {object} field The descriptor.
|
|
533
|
+
* @return {string}
|
|
534
|
+
*/
|
|
535
|
+
dateFieldLabel(field) {
|
|
536
|
+
const label = String(field.input.dateField.label || '')
|
|
537
|
+
return this.markOptional ? `${label} (${t('nextcloud-vue', 'optional')})` : label
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The days a chosen date stands for.
|
|
542
|
+
*
|
|
543
|
+
* @param {string} iso The date input's value.
|
|
544
|
+
* @return {string} The days, '' without a date.
|
|
545
|
+
*/
|
|
546
|
+
daysUntilIso(iso) {
|
|
547
|
+
return daysUntilIso(iso)
|
|
548
|
+
},
|
|
549
|
+
|
|
508
550
|
/**
|
|
509
551
|
* Label of a sub-field, with the required marker.
|
|
510
552
|
*
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
7
7
|
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
|
8
8
|
import { reactive } from 'vue'
|
|
9
|
+
import AccountOutline from 'vue-material-design-icons/AccountOutline.vue'
|
|
9
10
|
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
|
|
10
11
|
import Cancel from 'vue-material-design-icons/Cancel.vue'
|
|
11
12
|
import Check from 'vue-material-design-icons/Check.vue'
|
|
13
|
+
import EmailOutline from 'vue-material-design-icons/EmailOutline.vue'
|
|
12
14
|
import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
|
|
13
15
|
import LightningBolt from 'vue-material-design-icons/LightningBolt.vue'
|
|
14
16
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
@@ -19,6 +21,7 @@ import { fillTemplate } from '../utils/fillTemplate.js'
|
|
|
19
21
|
import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
|
|
20
22
|
import { fetchGroupMembers, fetchPresence, fetchStatuses, fetchUsers } from '../utils/nextcloudDirectory.js'
|
|
21
23
|
import { readPath } from '../utils/readPath.js'
|
|
24
|
+
import { resolveNextcloudUser } from '../utils/userAutocomplete.js'
|
|
22
25
|
import { useFlowStore } from './useFlowStore.js'
|
|
23
26
|
import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
|
|
24
27
|
import { useTaskInboxStore } from './useTaskInboxStore.js'
|
|
@@ -699,12 +702,15 @@ function tasksSource() {
|
|
|
699
702
|
*
|
|
700
703
|
* @param {string} key The column key.
|
|
701
704
|
* @param {string} order `asc` or `desc`.
|
|
702
|
-
*
|
|
705
|
+
* Open tasks come before finished ones (`openFirst`), each in that order.
|
|
706
|
+
*
|
|
707
|
+
* @return {{sort: string, openFirst: boolean}|null} The parameters, or null for a column the inbox cannot sort by.
|
|
703
708
|
* @spec openspec/changes/tasks-source-sorts-on-server/specs/index-sources/spec.md#requirement-a-named-source-can-sort-on-the-server
|
|
709
|
+
* @spec openspec/changes/tasks-source-open-first/specs/index-sources/spec.md#requirement-a-sorted-task-list-puts-open-tasks-first
|
|
704
710
|
*/
|
|
705
711
|
sortParam: (key, order) => {
|
|
706
712
|
const field = TASK_SORT_FIELDS[key]
|
|
707
|
-
return field ? { sort: (order === 'desc' ? '-' : '') + field } : null
|
|
713
|
+
return field ? { sort: (order === 'desc' ? '-' : '') + field, openFirst: true } : null
|
|
708
714
|
},
|
|
709
715
|
|
|
710
716
|
// The scope tabs. Each tab's filter is merged over the page's
|
|
@@ -1088,7 +1094,7 @@ function fillRowTemplate(template, row) {
|
|
|
1088
1094
|
* and its count (DqZoekresultaten).
|
|
1089
1095
|
*
|
|
1090
1096
|
* `sourceConfig.kinds` lists what is searched:
|
|
1091
|
-
* `{ id, label, register, schema, titleField?, subtitleField?, subtitleTemplate?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
|
|
1097
|
+
* `{ id, label, register, schema, titleField?, subtitleField?, subtitleTemplate?, statusField?, statusResolve?, linkedField?, linkedTemplate?, linkedUser?, href?, limit? }`
|
|
1092
1098
|
* for objects (OpenRegister's `_search`, so the reader sees what they may
|
|
1093
1099
|
* read), or `{ id, label, files: true, limit? }` for the reader's files
|
|
1094
1100
|
* (Nextcloud's unified search). The term is `sourceConfig.term`, else the
|
|
@@ -1104,6 +1110,8 @@ function fillRowTemplate(template, row) {
|
|
|
1104
1110
|
* column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
|
|
1105
1111
|
* `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
|
|
1106
1112
|
* result, else `subtitleField`.
|
|
1113
|
+
* `linkedUser: true` reads that value as a user id and shows the user's
|
|
1114
|
+
* display name. `sourceConfig.columnWidths` sets a width per column key.
|
|
1107
1115
|
* The page's title and count line can name the term as `{term}`, and the
|
|
1108
1116
|
* count is the active kind's total. Kind, link and status are filterable.
|
|
1109
1117
|
*
|
|
@@ -1113,6 +1121,23 @@ function fillRowTemplate(template, row) {
|
|
|
1113
1121
|
* @return {object} The source adapter.
|
|
1114
1122
|
* @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
|
|
1115
1123
|
*/
|
|
1124
|
+
/**
|
|
1125
|
+
* Columns with the widths a page names per column key
|
|
1126
|
+
* (`sourceConfig.columnWidths`, `{ title: '250px' }`); a column it does not
|
|
1127
|
+
* name keeps none.
|
|
1128
|
+
*
|
|
1129
|
+
* @param {Array<object>} columns The columns.
|
|
1130
|
+
* @param {object|undefined} widths The widths by key.
|
|
1131
|
+
* @return {Array<object>} The columns.
|
|
1132
|
+
* @spec openspec/changes/search-linked-user-and-widths/specs/search-linked-user-and-widths/spec.md
|
|
1133
|
+
*/
|
|
1134
|
+
function withColumnWidths(columns, widths) {
|
|
1135
|
+
if (!widths || typeof widths !== 'object') {
|
|
1136
|
+
return columns
|
|
1137
|
+
}
|
|
1138
|
+
return columns.map((col) => (typeof widths[col.key] === 'string' && widths[col.key] !== '' ? { ...col, width: widths[col.key] } : col))
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1116
1141
|
function searchSource(config = {}, context = {}) {
|
|
1117
1142
|
const translate = typeof context.translate === 'function' ? context.translate : (text) => text
|
|
1118
1143
|
const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
|
|
@@ -1208,12 +1233,20 @@ function searchSource(config = {}, context = {}) {
|
|
|
1208
1233
|
return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
|
|
1209
1234
|
}
|
|
1210
1235
|
|
|
1236
|
+
// `linkedUser`: the Linked cell holds a user id; name the user instead.
|
|
1237
|
+
const linkedUserNames = async (kind, objects) => {
|
|
1238
|
+
const ids = [...new Set(objects.map((object) => linkedOf(kind, object)).filter((id) => id !== ''))]
|
|
1239
|
+
const options = await Promise.all(ids.map((id) => resolveNextcloudUser(id).catch(() => null)))
|
|
1240
|
+
return new Map(ids.map((id, index) => [id, (options[index] && options[index].label) || id]))
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1211
1243
|
const searchObjects = async (axios, kind, term) => {
|
|
1212
1244
|
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
|
|
1213
1245
|
const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
|
|
1214
1246
|
const body = (res && res.data) || {}
|
|
1215
1247
|
const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
|
|
1216
1248
|
const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
|
|
1249
|
+
const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
|
|
1217
1250
|
return {
|
|
1218
1251
|
total: typeof body.total === 'number' ? body.total : results.length,
|
|
1219
1252
|
rows: results.map((object) => {
|
|
@@ -1230,7 +1263,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1230
1263
|
title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
|
|
1231
1264
|
subtitle: subtitleOf(kind, object),
|
|
1232
1265
|
status,
|
|
1233
|
-
linked: linkedOf(kind, object),
|
|
1266
|
+
linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
|
|
1234
1267
|
href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
|
|
1235
1268
|
object,
|
|
1236
1269
|
}
|
|
@@ -1338,7 +1371,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1338
1371
|
*/
|
|
1339
1372
|
loading: () => state.loading,
|
|
1340
1373
|
|
|
1341
|
-
columns: [
|
|
1374
|
+
columns: withColumnWidths([
|
|
1342
1375
|
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
|
|
1343
1376
|
// `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
|
|
1344
1377
|
// DqZoekresultaten), instead of the plural kind as a pill.
|
|
@@ -1353,7 +1386,7 @@ function searchSource(config = {}, context = {}) {
|
|
|
1353
1386
|
widgetProps: { colorMap: (config && config.statusColorMap) || {} },
|
|
1354
1387
|
sortable: true,
|
|
1355
1388
|
},
|
|
1356
|
-
],
|
|
1389
|
+
], config && config.columnWidths),
|
|
1357
1390
|
|
|
1358
1391
|
/**
|
|
1359
1392
|
* Every column header carries a filter button (DqZoekresultaten); the
|
|
@@ -1603,6 +1636,15 @@ function usersSource() {
|
|
|
1603
1636
|
/** A filter button on every head (DqAfdelingen); the members read are narrowed here. */
|
|
1604
1637
|
headerFilters: true,
|
|
1605
1638
|
|
|
1639
|
+
/**
|
|
1640
|
+
* A member's row menu (DqAfdelingen): their Nextcloud profile, and an
|
|
1641
|
+
* e-mail when they have an address. Both are links.
|
|
1642
|
+
*/
|
|
1643
|
+
rowActions: [
|
|
1644
|
+
{ id: 'open-profile', label: t('nextcloud-vue', 'Open profile'), icon: AccountOutline, href: (row) => generateUrl(`/u/${encodeURIComponent(row.id)}`) },
|
|
1645
|
+
{ id: 'send-email', label: t('nextcloud-vue', 'Send email'), icon: EmailOutline, href: (row) => `mailto:${row.email}`, visible: (row) => Boolean(row && row.email) },
|
|
1646
|
+
],
|
|
1647
|
+
|
|
1606
1648
|
showAdd: false,
|
|
1607
1649
|
}
|
|
1608
1650
|
}
|
|
@@ -41,7 +41,8 @@ export const FLOW_TASKS_URL = '/apps/openregister/api/flow-tasks'
|
|
|
41
41
|
// creating app named it, so it is the one filter that lets a Tasks page say
|
|
42
42
|
// "the reminders" without the engine learning what a reminder is. It widens
|
|
43
43
|
// nothing: it narrows within the inbox the session already decides.
|
|
44
|
-
|
|
44
|
+
// `openFirst` asks open tasks before finished ones, a sort the engine applies.
|
|
45
|
+
const ALLOWED_PARAMS = ['scope', 'state', 'isTerminal', 'priority', 'kind', 'overdue', 'dueAfter', 'dueBefore', 'objectUuid', 'sort', 'openFirst', 'limit', 'offset']
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* The flat body `POST /api/flow-tasks` takes for a task on a record.
|
|
@@ -178,7 +179,7 @@ export const useTaskInboxStore = defineStore('cnTaskInbox', {
|
|
|
178
179
|
if (value === undefined || value === null || value === '') {
|
|
179
180
|
continue
|
|
180
181
|
}
|
|
181
|
-
params[key] = (key === 'overdue' || key === 'isTerminal') ? String(value) : value
|
|
182
|
+
params[key] = (key === 'overdue' || key === 'isTerminal' || key === 'openFirst') ? String(value) : value
|
|
182
183
|
}
|
|
183
184
|
const response = await axios.get(generateUrl(FLOW_TASKS_URL), { params })
|
|
184
185
|
const results = response.data?.results || []
|
|
@@ -358,7 +359,7 @@ export const useTaskInboxStore = defineStore('cnTaskInbox', {
|
|
|
358
359
|
}
|
|
359
360
|
// The endpoint reads `overdue` as a boolean-ish string; a quick
|
|
360
361
|
// filter authors it as `true` for legibility.
|
|
361
|
-
params[key] = (key === 'overdue' || key === 'isTerminal') ? String(value) : value
|
|
362
|
+
params[key] = (key === 'overdue' || key === 'isTerminal' || key === 'openFirst') ? String(value) : value
|
|
362
363
|
}
|
|
363
364
|
|
|
364
365
|
this.loading = true
|
package/src/css/dialog.css
CHANGED
|
@@ -204,6 +204,24 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
204
204
|
gap: 10px;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/* A choice card with a checkbox (CnAssignDialog's toggles) is as tight as
|
|
208
|
+
the board draws it (DqOverdragen, 76px for a label and a two-line note):
|
|
209
|
+
10px 14px padding, the checkbox row at the 24px minimum target instead of
|
|
210
|
+
NcCheckboxRadioSwitch's 34px, the note at 1.2 line height. */
|
|
211
|
+
.cn-dialog-board .cn-assign-dialog__toggle {
|
|
212
|
+
padding: 10px 14px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch-checkbox .checkbox-content,
|
|
216
|
+
.cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch__input {
|
|
217
|
+
min-height: 24px;
|
|
218
|
+
padding-block: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cn-dialog-board .cn-assign-dialog__toggle-note {
|
|
222
|
+
line-height: 1.2;
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
/* Body: padding 20px 24px, 18px between blocks. */
|
|
208
226
|
.cn-dialog-board .dialog__content {
|
|
209
227
|
display: flex;
|
|
@@ -579,9 +579,16 @@
|
|
|
579
579
|
.cn-look-board .cn-conversation-thread .cn-conversation-thread__meta:not(.cn-look-nextcloud *),
|
|
580
580
|
.cn-look-board .cn-conversation-thread .cn-conversation-thread__meta time:not(.cn-look-nextcloud *) {
|
|
581
581
|
color: var(--cn-board-text-soft);
|
|
582
|
+
font-size: 13px;
|
|
582
583
|
line-height: normal;
|
|
583
584
|
}
|
|
584
585
|
|
|
586
|
+
/* 6px between the meta line and the text.
|
|
587
|
+
@spec openspec/changes/board-conversation-bubble-meta-gap/specs/board-conversation-bubble-meta-gap/spec.md#requirement-a-board-message-sets-its-meta-line-at-13px-6px-above-the-text */
|
|
588
|
+
.cn-look-board .cn-conversation-thread .cn-conversation-thread__bubble:not(.cn-look-nextcloud *) {
|
|
589
|
+
gap: 6px;
|
|
590
|
+
}
|
|
591
|
+
|
|
585
592
|
.cn-look-board .cn-conversation-thread .cn-conversation-thread__message--us .cn-conversation-thread__bubble:not(.cn-look-nextcloud *) {
|
|
586
593
|
color: var(--color-main-text);
|
|
587
594
|
}
|
|
@@ -1270,6 +1277,11 @@
|
|
|
1270
1277
|
line-height: normal;
|
|
1271
1278
|
}
|
|
1272
1279
|
|
|
1280
|
+
/* Head and body text at line height 1.2, as the board draws the table. */
|
|
1281
|
+
.cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table :is(th, td) {
|
|
1282
|
+
line-height: 1.2;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1273
1285
|
/* A long task name wraps under itself, as the board draws it, rather than
|
|
1274
1286
|
widening the table past the card. */
|
|
1275
1287
|
.cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table :is(.cn-tasks-widget__name, .cn-tasks-widget__meta) {
|
|
@@ -1569,3 +1581,15 @@
|
|
|
1569
1581
|
font-size: 14px;
|
|
1570
1582
|
line-height: 1.2;
|
|
1571
1583
|
}
|
|
1584
|
+
|
|
1585
|
+
/* The contacts tab inside a detail card (DqZaakBetrokkenen "Partijen") takes
|
|
1586
|
+
the card's padding instead of its own, and its buttons start at the card's
|
|
1587
|
+
edge under the subtitle.
|
|
1588
|
+
@spec openspec/changes/board-contacts-in-card/specs/contacts/spec.md */
|
|
1589
|
+
.cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab {
|
|
1590
|
+
padding: 0;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab .cn-contacts-tab__header {
|
|
1594
|
+
justify-content: flex-start;
|
|
1595
|
+
}
|
|
@@ -71,6 +71,27 @@
|
|
|
71
71
|
margin-inline-start: 0;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/* With the side panel open the buttons take the row under the title, the
|
|
75
|
+
breadcrumb and the count line follow (DqZakenSorteren).
|
|
76
|
+
@spec openspec/changes/board-header-buttons-under-title-beside-side-panel/specs/board-header-buttons-under-title-beside-side-panel/spec.md#requirement-the-header-buttons-sit-under-the-title-while-the-side-panel-is-open */
|
|
77
|
+
.cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-index-page__header-buttons,
|
|
78
|
+
.cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-index-page__header-buttons {
|
|
79
|
+
grid-column: 1 / -1;
|
|
80
|
+
grid-row: 2;
|
|
81
|
+
justify-self: start;
|
|
82
|
+
margin-block: 6px 3px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-header-trail,
|
|
86
|
+
.cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-header-trail {
|
|
87
|
+
grid-row: 3;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-page-header__text > .cn-page-header__description,
|
|
91
|
+
.cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-page-header__text > .cn-page-header__description {
|
|
92
|
+
grid-row: 4;
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
/* The header buttons keep the board's button under a theme that styles every
|
|
75
96
|
secondary or primary button with `!important` (the nldesign theme: a
|
|
76
97
|
primary border, 8px 16px padding): Download and the Actions menu are 40px,
|
package/src/css/look-board.css
CHANGED
|
@@ -321,6 +321,20 @@
|
|
|
321
321
|
border: 0;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/* A dashboard banner's kicker is a 13px bold line in sentence case, no
|
|
325
|
+
tracking (DqMijnWerk "Vandaag eerst", BqMijnGoedkeuringen "First, today");
|
|
326
|
+
the plain look keeps its spaced capitals. The kicker is a paragraph, so its
|
|
327
|
+
size and weight also go to the theme's paragraph tokens.
|
|
328
|
+
@spec openspec/changes/board-banner-kicker-case/specs/board-banner-kicker-case/spec.md#requirement-a-board-dashboard-banners-kicker-is-in-sentence-case */
|
|
329
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-banner-widget__kicker {
|
|
330
|
+
--nldesign-component-paragraph-font-size: 13px;
|
|
331
|
+
--nldesign-component-paragraph-font-weight: 700;
|
|
332
|
+
font-size: 13px;
|
|
333
|
+
font-weight: 700;
|
|
334
|
+
letter-spacing: 0;
|
|
335
|
+
text-transform: none;
|
|
336
|
+
}
|
|
337
|
+
|
|
324
338
|
/* ---- A workspace filter form (CnWorkspaceFilterWidget) ----------------- */
|
|
325
339
|
/* The boards draw the form's field and its load button 40px tall, the button
|
|
326
340
|
16px in at both sides (DqTermijnen "Jaar" with "Rapport laden"); the form
|
|
@@ -647,6 +661,13 @@
|
|
|
647
661
|
--button-size: 34px;
|
|
648
662
|
}
|
|
649
663
|
|
|
664
|
+
/* Two buttons in a card's title row stand 8px apart (DqZaakContact "Live
|
|
665
|
+
gesprek", DqZaakEmail "Mail koppelen" / "Mail opstellen").
|
|
666
|
+
@spec openspec/changes/board-card-header-actions-gap/specs/board-card-header-actions-gap/spec.md#requirement-a-board-cards-title-row-buttons-stand-8px-apart */
|
|
667
|
+
.cn-look-board .cn-widget-wrapper__actions:not(.cn-look-nextcloud *) {
|
|
668
|
+
gap: 8px;
|
|
669
|
+
}
|
|
670
|
+
|
|
650
671
|
/* A detail page marked `cardButtons: "regular"` draws them at the 40px
|
|
651
672
|
control height: the case page's tab cards (DqZaakContact "Live gesprek",
|
|
652
673
|
DqZaakHistorie, DqZaakBetrokkenen), where setting and case-type pages keep 34.
|
|
@@ -2983,8 +3004,8 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
|
|
|
2983
3004
|
|
|
2984
3005
|
/* A dialog's options as the boards draw them (DqVolgendeStap, DqBesluit,
|
|
2985
3006
|
DqDocumentGenereren, DqFilterBewerken): rounded at 10px, an idle option
|
|
2986
|
-
edged in the dark border grey, and an 18px radio. The chosen
|
|
2987
|
-
|
|
3007
|
+
edged in the dark border grey, and an 18px radio. The chosen option keeps
|
|
3008
|
+
its own edge. */
|
|
2988
3009
|
.cn-look-board .cn-choice-cards .cn-choice-cards__row:not(.cn-look-nextcloud *),
|
|
2989
3010
|
.cn-look-board .cn-choice-cards .cn-choice-cards__cell--with-details:not(.cn-look-nextcloud *) {
|
|
2990
3011
|
border-radius: 10px;
|
|
@@ -2994,6 +3015,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
|
|
|
2994
3015
|
border-color: var(--color-border-maxcontrast);
|
|
2995
3016
|
}
|
|
2996
3017
|
|
|
3018
|
+
/* A refused option keeps that edge and sits on the sunken ground
|
|
3019
|
+
(DqVolgendeStap: Hervatten, Archiveren, Heropenen). */
|
|
3020
|
+
.cn-look-board .cn-choice-cards .cn-choice-cards__row--disabled:not(.cn-look-nextcloud *),
|
|
3021
|
+
.cn-look-board .cn-choice-cards .cn-choice-cards__row--disabled:hover:not(.cn-look-nextcloud *) {
|
|
3022
|
+
border-color: var(--color-border-maxcontrast);
|
|
3023
|
+
background: var(--cn-board-sunken-ground);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2997
3026
|
/* A chosen option stays white under its primary edge, as every Dq dialog board
|
|
2998
3027
|
draws it (design-system #371); the Nextcloud look keeps the tint.
|
|
2999
3028
|
@spec openspec/changes/board-choice-row-chosen-white/specs/board-choice-row-chosen-white/spec.md#requirement-a-chosen-board-option-is-white */
|
package/src/css/table.css
CHANGED
|
@@ -289,6 +289,19 @@
|
|
|
289
289
|
white-space: nowrap;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
/* A growing column that opts into wrapping (`cn-cell--wrap`, DqDoorlooptijd
|
|
293
|
+
"Zaken met risico" titles) wraps its text over lines instead of cutting
|
|
294
|
+
it, and keeps a readable width: with `max-width: 0` the wide one-line
|
|
295
|
+
cells beside it squeezed it to its padding (24px).
|
|
296
|
+
@spec openspec/changes/fit-table-wrap-column-keeps-width/specs/fit-table-wrap-column-keeps-width/spec.md#requirement-a-wrapping-growing-column-keeps-a-readable-width */
|
|
297
|
+
.cn-data-table--fit th.cn-table-col--grow.cn-cell--wrap,
|
|
298
|
+
.cn-data-table--fit td.cn-table-col--grow.cn-cell--wrap {
|
|
299
|
+
min-width: 8em;
|
|
300
|
+
max-width: none;
|
|
301
|
+
overflow-wrap: anywhere;
|
|
302
|
+
white-space: normal;
|
|
303
|
+
}
|
|
304
|
+
|
|
292
305
|
/* The secondary line of the growing column ("2026-0004" under a case title)
|
|
293
306
|
is a line of its own with its own ellipsis. The cell does not wrap, so in
|
|
294
307
|
the board look, where the secondary line is an inline block (the title
|
|
@@ -470,6 +470,27 @@ export function formatMasked(value, _row, _property, options) {
|
|
|
470
470
|
return '*'.repeat(Math.min(text.length - visible, 5)) + (visible > 0 ? text.slice(-visible) : '')
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
/**
|
|
474
|
+
* A stored value through a display map (`valueMap` registry key): the label
|
|
475
|
+
* `options.map` gives the value, else the value as stored. "Besloten
|
|
476
|
+
* Vennootschap" reads "BV" on DqOrganisaties.
|
|
477
|
+
*
|
|
478
|
+
* @param {unknown} value The stored value.
|
|
479
|
+
* @param {object} [_row] The full row (unused).
|
|
480
|
+
* @param {object} [_property] The schema property (unused).
|
|
481
|
+
* @param {{map?: Object<string, string>}} [options] The column's `formatterOptions`.
|
|
482
|
+
* @return {string} The label, or the value.
|
|
483
|
+
* @spec openspec/changes/value-map-formatter/specs/index-page/spec.md#requirement-a-column-can-show-stored-values-through-a-display-map
|
|
484
|
+
*/
|
|
485
|
+
export function formatValueMap(value, _row, _property, options) {
|
|
486
|
+
if (value === null || value === undefined || value === '') {
|
|
487
|
+
return ''
|
|
488
|
+
}
|
|
489
|
+
const map = options && options.map && typeof options.map === 'object' ? options.map : {}
|
|
490
|
+
const label = Object.hasOwn(map, String(value)) ? map[String(value)] : undefined
|
|
491
|
+
return typeof label === 'string' && label !== '' ? label : String(value)
|
|
492
|
+
}
|
|
493
|
+
|
|
473
494
|
/**
|
|
474
495
|
* Built-in formatter registry merged under any consumer-registered
|
|
475
496
|
* `formatters` in `CnAppRoot`'s `cnFormatters` provide.
|
|
@@ -516,4 +537,5 @@ export const BUILT_IN_FORMATTERS = {
|
|
|
516
537
|
connectionSettingsLabel: formatConnectionSettingsLabel,
|
|
517
538
|
masked: formatMasked,
|
|
518
539
|
isoDuration: formatIsoDuration,
|
|
540
|
+
valueMap: formatValueMap,
|
|
519
541
|
}
|
|
@@ -64,7 +64,7 @@ export function formatLongDate(value, now = new Date(), withYear = false) {
|
|
|
64
64
|
* Format one token value.
|
|
65
65
|
*
|
|
66
66
|
* @param {unknown} value The raw value.
|
|
67
|
-
* @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `ago`, `week`, `days`, `lower` or '' (as is).
|
|
67
|
+
* @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `ago`, `week`, `days`, `lower`, `mailbox` or '' (as is).
|
|
68
68
|
* @param {Date} now The reference moment.
|
|
69
69
|
* @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
|
|
70
70
|
* @param {object|null} [object] The row, for a currency read from a field.
|
|
@@ -125,6 +125,12 @@ function formatValue(value, format, now, arg = '', object = null, config = null,
|
|
|
125
125
|
if (format === 'ago') {
|
|
126
126
|
return formatAgo(value, now)
|
|
127
127
|
}
|
|
128
|
+
if (format === 'mailbox') {
|
|
129
|
+
// An address up to and with its @ ("gemeente@" from gemeente@zuiddrecht.nl).
|
|
130
|
+
const text = String(value)
|
|
131
|
+
const at = text.indexOf('@')
|
|
132
|
+
return at > 0 ? text.slice(0, at + 1) : text
|
|
133
|
+
}
|
|
128
134
|
if (format === 'lower') {
|
|
129
135
|
return String(value).toLocaleLowerCase()
|
|
130
136
|
}
|
package/src/utils/myTeam.js
CHANGED
|
@@ -30,12 +30,32 @@ let _source = null
|
|
|
30
30
|
/** The read in flight, shared by every caller. */
|
|
31
31
|
let _pending = null
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The team the app answered, as kept: one group id, a list of two or more,
|
|
35
|
+
* or `''` for none. Blank entries are dropped and a list of one reads as that
|
|
36
|
+
* one id.
|
|
37
|
+
*
|
|
38
|
+
* @spec openspec/changes/my-team-source-may-answer-several-teams/specs/schema-utilities/spec.md#requirement-myteam-may-resolve-to-several-teams
|
|
39
|
+
* @param {unknown} team What the app's loader answered.
|
|
40
|
+
* @return {string|string[]} The kept team.
|
|
41
|
+
*/
|
|
42
|
+
function normalizeTeam(team) {
|
|
43
|
+
if (Array.isArray(team)) {
|
|
44
|
+
const ids = team.filter((id) => typeof id === 'string').map((id) => id.trim()).filter((id) => id !== '')
|
|
45
|
+
if (ids.length === 0) {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
return ids.length === 1 ? ids[0] : ids
|
|
49
|
+
}
|
|
50
|
+
return typeof team === 'string' ? team.trim() : ''
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
/**
|
|
34
54
|
* Name where the current user's primary team is read: a function answering
|
|
35
55
|
* the team's Nextcloud group id, or null/'' for a user in no team.
|
|
36
56
|
*
|
|
37
57
|
* @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
|
|
38
|
-
* @param {(() => Promise<string|null>)|null} loader The app's loader, or null to unregister.
|
|
58
|
+
* @param {(() => Promise<string|string[]|null>)|null} loader The app's loader (a group id, or the ids of several teams), or null to unregister.
|
|
39
59
|
* @return {void}
|
|
40
60
|
*/
|
|
41
61
|
export function registerMyTeamSource(loader) {
|
|
@@ -49,7 +69,7 @@ export function registerMyTeamSource(loader) {
|
|
|
49
69
|
* when no source is registered, or when the read fails.
|
|
50
70
|
*
|
|
51
71
|
* @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
|
|
52
|
-
* @return {Promise<string>} The team's group id, or ''.
|
|
72
|
+
* @return {Promise<string|string[]>} The team's group id (or ids), or ''.
|
|
53
73
|
*/
|
|
54
74
|
export function loadMyTeam() {
|
|
55
75
|
if (_state.team !== null) {
|
|
@@ -64,7 +84,7 @@ export function loadMyTeam() {
|
|
|
64
84
|
}
|
|
65
85
|
_pending = Promise.resolve()
|
|
66
86
|
.then(() => _source())
|
|
67
|
-
.then((team) => (
|
|
87
|
+
.then((team) => normalizeTeam(team))
|
|
68
88
|
.catch(() => '')
|
|
69
89
|
.then((team) => {
|
|
70
90
|
_state.team = team
|
|
@@ -79,7 +99,7 @@ export function loadMyTeam() {
|
|
|
79
99
|
* it is being read (and starts the read on first use).
|
|
80
100
|
*
|
|
81
101
|
* @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
|
|
82
|
-
* @return {string|null} The team, '' or null.
|
|
102
|
+
* @return {string|string[]|null} The team (or teams), '' or null.
|
|
83
103
|
*/
|
|
84
104
|
export function peekMyTeam() {
|
|
85
105
|
if (_state.team === null) {
|
|
@@ -89,15 +89,16 @@ export const MY_GROUPS_TOKEN = '@myGroups'
|
|
|
89
89
|
export const MY_TEAM_TOKEN = '@myTeam'
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Resolve `@myTeam`: the team's group id
|
|
92
|
+
* Resolve `@myTeam`: the team's group id, or the ids of several teams when the
|
|
93
|
+
* app answers a list (an IN filter); `@myTeam` while it is being read (the
|
|
93
94
|
* caller waits); `@myTeam?` for a user in no team (the caller drops the key).
|
|
94
95
|
*
|
|
95
96
|
* @spec openspec/changes/saved-view-tokens-my-team/specs/schema-utilities/spec.md#requirement-myteam-resolves-to-the-current-users-primary-team
|
|
96
|
-
* @param {{myTeam?: string}} [ctx] Optional context; `ctx.myTeam` wins.
|
|
97
|
-
* @return {string} The group id or the token.
|
|
97
|
+
* @param {{myTeam?: (string|string[])}} [ctx] Optional context; `ctx.myTeam` wins.
|
|
98
|
+
* @return {string|string[]} The group id(s) or the token.
|
|
98
99
|
*/
|
|
99
100
|
function resolveMyTeam(ctx) {
|
|
100
|
-
const team = ctx && typeof ctx.myTeam === 'string' ? ctx.myTeam : peekMyTeam()
|
|
101
|
+
const team = ctx && (typeof ctx.myTeam === 'string' || Array.isArray(ctx.myTeam)) ? ctx.myTeam : peekMyTeam()
|
|
101
102
|
if (team === null) {
|
|
102
103
|
return MY_TEAM_TOKEN
|
|
103
104
|
}
|
|
@@ -223,6 +223,41 @@ function daysAfter(days, now) {
|
|
|
223
223
|
return date
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Today plus that many days, as a date input's value.
|
|
228
|
+
*
|
|
229
|
+
* @param {unknown} days Whole days, 1 or more.
|
|
230
|
+
* @param {Date} [now] What counts as today.
|
|
231
|
+
* @return {string} `yyyy-mm-dd`, '' without a positive whole number.
|
|
232
|
+
* @spec openspec/changes/days-input-as-date/specs/transition-inputs/spec.md
|
|
233
|
+
*/
|
|
234
|
+
export function dayAfterIso(days, now = new Date()) {
|
|
235
|
+
const date = daysAfter(days, now)
|
|
236
|
+
if (date === null) {
|
|
237
|
+
return ''
|
|
238
|
+
}
|
|
239
|
+
const pad = (number) => String(number).padStart(2, '0')
|
|
240
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The whole days from today to a date input's value, at least 1.
|
|
245
|
+
*
|
|
246
|
+
* @param {unknown} iso A `yyyy-mm-dd` date.
|
|
247
|
+
* @param {Date} [now] What counts as today.
|
|
248
|
+
* @return {string} The days, '' without a date.
|
|
249
|
+
* @spec openspec/changes/days-input-as-date/specs/transition-inputs/spec.md
|
|
250
|
+
*/
|
|
251
|
+
export function daysUntilIso(iso, now = new Date()) {
|
|
252
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso ?? '').trim())
|
|
253
|
+
if (!match) {
|
|
254
|
+
return ''
|
|
255
|
+
}
|
|
256
|
+
const target = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
|
|
257
|
+
const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
|
|
258
|
+
return String(Math.max(1, Math.round((target - today) / 86400000)))
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
/**
|
|
227
262
|
* A date as a reader sees it: day, short month and year.
|
|
228
263
|
*
|
|
@@ -290,6 +325,9 @@ export function fillInputTemplate(template, values, now = new Date()) {
|
|
|
290
325
|
* in that other input.
|
|
291
326
|
* - `preview: { label, template, note, testid }`: a line under the input,
|
|
292
327
|
* filled by `fillInputTemplate` and shown only once it resolves.
|
|
328
|
+
* - `dateField: { label, note, testid }`: on a days input, an editable date
|
|
329
|
+
* under it, today plus the days; a chosen date sets the days to the whole
|
|
330
|
+
* days from today, at least 1. `dateValue` carries the date.
|
|
293
331
|
* - `default`: the starting value.
|
|
294
332
|
* - Presentation: `labelOutside`, `id`, `note`, `wide`, `testid`, `rows`,
|
|
295
333
|
* `placeholder`.
|
|
@@ -302,7 +340,7 @@ export function fillInputTemplate(template, values, now = new Date()) {
|
|
|
302
340
|
* @param {string} [options.register] Register the reference pickers fall back to.
|
|
303
341
|
* @param {object} [options.context] What `optionsSource` reads.
|
|
304
342
|
* @param {object} [options.values] The values so far.
|
|
305
|
-
* @param {Date} [options.now] What counts as today, for previews
|
|
343
|
+
* @param {Date} [options.now] What counts as today, for previews and `dateField`.
|
|
306
344
|
* @return {Array<object>} The descriptors.
|
|
307
345
|
*/
|
|
308
346
|
export function resolveInputFields(declared, { schema = null, register = '', context = {}, values = {}, now } = {}) {
|
|
@@ -345,6 +383,9 @@ export function resolveInputFields(declared, { schema = null, register = '', con
|
|
|
345
383
|
field.previewText = preview && typeof preview === 'object'
|
|
346
384
|
? fillInputTemplate(preview.template, values, now)
|
|
347
385
|
: ''
|
|
386
|
+
field.dateValue = field.input.dateField && typeof field.input.dateField === 'object'
|
|
387
|
+
? dayAfterIso(values[field.key], now)
|
|
388
|
+
: ''
|
|
348
389
|
}
|
|
349
390
|
|
|
350
391
|
return fields
|