@conduction/nextcloud-vue 2.82.0 → 2.83.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 (78) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +9 -0
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  17. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  18. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  19. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  20. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  21. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  22. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  24. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  26. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +43 -0
  27. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +130 -53
  29. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  31. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  32. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  34. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  36. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +3 -1
  37. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  40. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +55 -3
  41. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  43. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  45. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  46. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  48. package/dist/esm/l10n/nl.json.js +3 -3
  49. package/dist/esm/library-style.js +1 -1
  50. package/dist/esm/nextcloud-vue.css +549 -338
  51. package/dist/esm/utils/fetchAggregate.js +25 -2
  52. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  53. package/dist/esm/utils/switchValues.js +62 -0
  54. package/dist/esm/utils/switchValues.js.map +1 -0
  55. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  56. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  57. package/dist/nextcloud-vue.cjs.js +36086 -35536
  58. package/dist/nextcloud-vue.cjs.js.map +2 -2
  59. package/dist/nextcloud-vue.css +549 -338
  60. package/l10n/nl.json +3 -3
  61. package/package.json +1 -1
  62. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  63. package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -0
  64. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  65. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  66. package/src/components/CnReportsPage/CnReportsPage.vue +84 -4
  67. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  68. package/src/components/CnStatWidget/CnStatWidget.vue +3 -1
  69. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +92 -5
  70. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  71. package/src/css/index-page.css +11 -0
  72. package/src/css/index.css +1 -0
  73. package/src/css/look-board-reports.css +117 -0
  74. package/src/css/look-board.css +32 -1
  75. package/src/schemas/app-manifest-v2.schema.json +2 -2
  76. package/src/utils/fetchAggregate.js +24 -1
  77. package/src/utils/switchValues.js +59 -0
  78. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -2,7 +2,7 @@ import { translate } from '@nextcloud/l10n';
2
2
  import { NcActions, NcActionButton } from '@nextcloud/vue';
3
3
  import ContentCopy from 'vue-material-design-icons/ContentCopy.vue';
4
4
  import Export from 'vue-material-design-icons/Export.vue';
5
- import Import from 'vue-material-design-icons/Import.vue';
5
+ import ImportIcon from 'vue-material-design-icons/Import.vue';
6
6
  import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue';
7
7
  import TuneVariant from 'vue-material-design-icons/TuneVariant.vue';
8
8
 
@@ -31,7 +31,7 @@ var script = {
31
31
  TuneVariant,
32
32
  ContentCopy,
33
33
  TrashCanOutline,
34
- Import,
34
+ Import: ImportIcon,
35
35
  Export,
36
36
  },
37
37
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnMassActionBar.vue2.js","sources":["../../../../src/components/CnMassActionBar/CnMassActionBar.vue"],"sourcesContent":["<template>\n\t<NcActions\n\t\tv-if=\"count > 0\"\n\t\t:menuName=\"menuLabel\"\n\t\t:inline=\"0\">\n\t\t<template #icon>\n\t\t\t<TuneVariant :size=\"20\" />\n\t\t</template>\n\n\t\t<!-- Built-in: Import -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showImport\"\n\t\t\t@click=\"emitMassImport\">\n\t\t\t<template #icon>\n\t\t\t\t<Import :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ importLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Export -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showExport\"\n\t\t\t@click=\"emitMassExport\">\n\t\t\t<template #icon>\n\t\t\t\t<Export :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ exportLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Copy -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showCopy\"\n\t\t\t@click=\"emitMassCopy\">\n\t\t\t<template #icon>\n\t\t\t\t<ContentCopy :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ copyLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Delete -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showDelete\"\n\t\t\t@click=\"emitMassDelete\">\n\t\t\t<template #icon>\n\t\t\t\t<TrashCanOutline :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ deleteLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- @slot actions Additional app-specific mass-action buttons. Slot scope: `{ count, selectedIds }`. -->\n\t\t<slot name=\"actions\" :count=\"count\" :selectedIds=\"selectedIds\" />\n\t</NcActions>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActions } from '@nextcloud/vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport Export from 'vue-material-design-icons/Export.vue'\nimport Import from 'vue-material-design-icons/Import.vue'\nimport TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'\nimport TuneVariant from 'vue-material-design-icons/TuneVariant.vue'\n\n/**\n * CnMassActionBar — Mass action dropdown button for selected items.\n *\n * Renders as a single \"Mass Actions (N)\" dropdown button with built-in\n * Import, Export, Copy, and Delete actions plus a slot for custom actions.\n * Place this in the header/action bar of your index page.\n *\n * Use the `#actions` scoped slot to add app-specific mass actions\n * (use `NcActionButton` for slot content).\n *\n * @event mass-import Emitted when the Import action is clicked. No payload.\n * @event mass-export Emitted when the Export action is clicked. No payload.\n * @event mass-copy Emitted when the Copy action is clicked. No payload.\n * @event mass-delete Emitted when the Delete action is clicked. No payload.\n * @slot actions Additional app-specific mass-action buttons. Slot scope: `{ count, selectedIds }`.\n */\nexport default {\n\tname: 'CnMassActionBar',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tTuneVariant,\n\t\tContentCopy,\n\t\tTrashCanOutline,\n\t\tImport,\n\t\tExport,\n\t},\n\n\tprops: {\n\t\t/** Array of selected item IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Number of selected items */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Whether to show the built-in Import action */\n\t\tshowImport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Export action */\n\t\tshowExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Copy action */\n\t\tshowCopy: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Delete action */\n\t\tshowDelete: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label template for the menu button. Use {count} for the count. */\n\t\tmenuLabelTemplate: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Mass actions ({count})'),\n\t\t},\n\n\t\t/** Label for the built-in Import mass-action button. */\n\t\timportLabel: { type: String, default: () => t('nextcloud-vue', 'Import') },\n\t\t/** Label for the built-in Export mass-action button. */\n\t\texportLabel: { type: String, default: () => t('nextcloud-vue', 'Export') },\n\t\t/** Label for the built-in Copy mass-action button. */\n\t\tcopyLabel: { type: String, default: () => t('nextcloud-vue', 'Copy') },\n\t\t/** Label for the built-in Delete mass-action button. */\n\t\tdeleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },\n\t},\n\n\temits: ['mass-copy', 'mass-delete', 'mass-export', 'mass-import'],\n\n\tcomputed: {\n\t\tmenuLabel() {\n\t\t\treturn this.menuLabelTemplate.replace('{count}', String(this.count))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Emit the built-in Import click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassImport() {\n\t\t\t/**\n\t\t\t * @event mass-import Emitted when the Import action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-import')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Export click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassExport() {\n\t\t\t/**\n\t\t\t * @event mass-export Emitted when the Export action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-export')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Copy click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassCopy() {\n\t\t\t/**\n\t\t\t * @event mass-copy Emitted when the Copy action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-copy')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Delete click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassDelete() {\n\t\t\t/**\n\t\t\t * @event mass-delete Emitted when the Delete action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-delete')\n\t\t},\n\t},\n}\n</script>\n"],"names":["t"],"mappings":";;;;;;;;AA+DA;;AAEC;;;;AAIA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAED,aAAe;CACd,IAAI,EAAE,iBAAiB;;CAEvB,UAAU,EAAE;AACX,EAAA,SAAS;AACT,EAAA,cAAc;EACd,WAAW;EACX,WAAW;AACX,EAAA,eAAe;AACf,EAAA,MAAM;AACN,EAAA,MAAM;EACN;;AAED,CAAA,KAAK,EAAE;;EAEN,WAAW,EAAE;GACZ,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;;AAGD,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,CAAC;GACV;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;AACA,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;;AAGD,EAAA,iBAAiB,EAAE;GAClB,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,wBAAwB,CAAC;GAC3D;;;AAGD,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;;AAE1E,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;AAC1E;EACA,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,MAAM,CAAA,EAAG;;AAEtE,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;EAC1E;;CAED,KAAK,EAAE,CAAC,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,CAAC;;AAEjE,CAAA,QAAQ,EAAE;EACT,SAAS,GAAG;AACX,GAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;GACnE;EACD;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;;AAED;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;;AAED;AACC;AACA;;;AAGD,EAAA,YAAY,GAAG;AACd;;;GAGA,IAAI,CAAC,KAAK,CAAC,WAAW,EAAA;GACtB;;AAED;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnMassActionBar.vue2.js","sources":["../../../../src/components/CnMassActionBar/CnMassActionBar.vue"],"sourcesContent":["<template>\n\t<NcActions\n\t\tv-if=\"count > 0\"\n\t\t:menuName=\"menuLabel\"\n\t\t:inline=\"0\">\n\t\t<template #icon>\n\t\t\t<TuneVariant :size=\"20\" />\n\t\t</template>\n\n\t\t<!-- Built-in: Import -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showImport\"\n\t\t\t@click=\"emitMassImport\">\n\t\t\t<template #icon>\n\t\t\t\t<Import :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ importLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Export -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showExport\"\n\t\t\t@click=\"emitMassExport\">\n\t\t\t<template #icon>\n\t\t\t\t<Export :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ exportLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Copy -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showCopy\"\n\t\t\t@click=\"emitMassCopy\">\n\t\t\t<template #icon>\n\t\t\t\t<ContentCopy :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ copyLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- Built-in: Delete -->\n\t\t<NcActionButton\n\t\t\tv-if=\"showDelete\"\n\t\t\t@click=\"emitMassDelete\">\n\t\t\t<template #icon>\n\t\t\t\t<TrashCanOutline :size=\"20\" />\n\t\t\t</template>\n\t\t\t{{ deleteLabel }}\n\t\t</NcActionButton>\n\n\t\t<!-- @slot actions Additional app-specific mass-action buttons. Slot scope: `{ count, selectedIds }`. -->\n\t\t<slot name=\"actions\" :count=\"count\" :selectedIds=\"selectedIds\" />\n\t</NcActions>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActions } from '@nextcloud/vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport Export from 'vue-material-design-icons/Export.vue'\nimport Import from 'vue-material-design-icons/Import.vue'\nimport TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'\nimport TuneVariant from 'vue-material-design-icons/TuneVariant.vue'\n\n/**\n * CnMassActionBar — Mass action dropdown button for selected items.\n *\n * Renders as a single \"Mass Actions (N)\" dropdown button with built-in\n * Import, Export, Copy, and Delete actions plus a slot for custom actions.\n * Place this in the header/action bar of your index page.\n *\n * Use the `#actions` scoped slot to add app-specific mass actions\n * (use `NcActionButton` for slot content).\n *\n * @event mass-import Emitted when the Import action is clicked. No payload.\n * @event mass-export Emitted when the Export action is clicked. No payload.\n * @event mass-copy Emitted when the Copy action is clicked. No payload.\n * @event mass-delete Emitted when the Delete action is clicked. No payload.\n * @slot actions Additional app-specific mass-action buttons. Slot scope: `{ count, selectedIds }`.\n */\nexport default {\n\tname: 'CnMassActionBar',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tTuneVariant,\n\t\tContentCopy,\n\t\tTrashCanOutline,\n\t\tImport,\n\t\tExport,\n\t},\n\n\tprops: {\n\t\t/** Array of selected item IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Number of selected items */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Whether to show the built-in Import action */\n\t\tshowImport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Export action */\n\t\tshowExport: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Copy action */\n\t\tshowCopy: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Whether to show the built-in Delete action */\n\t\tshowDelete: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Label template for the menu button. Use {count} for the count. */\n\t\tmenuLabelTemplate: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Mass actions ({count})'),\n\t\t},\n\n\t\t/** Label for the built-in Import mass-action button. */\n\t\timportLabel: { type: String, default: () => t('nextcloud-vue', 'Import') },\n\t\t/** Label for the built-in Export mass-action button. */\n\t\texportLabel: { type: String, default: () => t('nextcloud-vue', 'Export') },\n\t\t/** Label for the built-in Copy mass-action button. */\n\t\tcopyLabel: { type: String, default: () => t('nextcloud-vue', 'Copy') },\n\t\t/** Label for the built-in Delete mass-action button. */\n\t\tdeleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },\n\t},\n\n\temits: ['mass-copy', 'mass-delete', 'mass-export', 'mass-import'],\n\n\tcomputed: {\n\t\tmenuLabel() {\n\t\t\treturn this.menuLabelTemplate.replace('{count}', String(this.count))\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Emit the built-in Import click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassImport() {\n\t\t\t/**\n\t\t\t * @event mass-import Emitted when the Import action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-import')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Export click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassExport() {\n\t\t\t/**\n\t\t\t * @event mass-export Emitted when the Export action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-export')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Copy click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassCopy() {\n\t\t\t/**\n\t\t\t * @event mass-copy Emitted when the Copy action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-copy')\n\t\t},\n\n\t\t/**\n\t\t * Emit the built-in Delete click. No payload.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\temitMassDelete() {\n\t\t\t/**\n\t\t\t * @event mass-delete Emitted when the Delete action is clicked. No payload.\n\t\t\t */\n\t\t\tthis.$emit('mass-delete')\n\t\t},\n\t},\n}\n</script>\n"],"names":["Import","t"],"mappings":";;;;;;;;AA+DA;;AAEC;;;;AAIA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAED,aAAe;CACd,IAAI,EAAE,iBAAiB;;CAEvB,UAAU,EAAE;AACX,EAAA,SAAS;AACT,EAAA,cAAc;EACd,WAAW;EACX,WAAW;AACX,EAAA,eAAe;AACf,UAAAA,UAAM;AACN,EAAA,MAAM;EACN;;AAED,CAAA,KAAK,EAAE;;EAEN,WAAW,EAAE;GACZ,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;;AAGD,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,CAAC;GACV;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;AACA,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,IAAI;GACb;;;AAGD,EAAA,iBAAiB,EAAE;GAClB,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,MAAMC,SAAC,CAAC,eAAe,EAAE,wBAAwB,CAAC;GAC3D;;;AAGD,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;;AAE1E,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;AAC1E;EACA,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,MAAM,CAAA,EAAG;;AAEtE,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,QAAQ,GAAG;EAC1E;;CAED,KAAK,EAAE,CAAC,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,CAAC;;AAEjE,CAAA,QAAQ,EAAE;EACT,SAAS,GAAG;AACX,GAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;GACnE;EACD;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;;AAED;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;;AAED;AACC;AACA;;;AAGD,EAAA,YAAY,GAAG;AACd;;;GAGA,IAAI,CAAC,KAAK,CAAC,WAAW,EAAA;GACtB;;AAED;AACC;AACA;;;AAGD,EAAA,cAAc,GAAG;AAChB;AACC;;GAED,IAAI,CAAC,KAAK,CAAC,aAAa,EAAA;GACxB;EACD;AACF;;;;"}
@@ -1,7 +1,7 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
2
  import { NcButton, NcNoteCard, NcLoadingIcon, NcCheckboxRadioSwitch } from '@nextcloud/vue';
3
3
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue';
4
- import Import from 'vue-material-design-icons/Import.vue';
4
+ import ImportIcon from 'vue-material-design-icons/Import.vue';
5
5
  import Upload from 'vue-material-design-icons/Upload.vue';
6
6
  import __sfc_main from '../CnDialog/CnDialog.vue.js';
7
7
  import { dialogBoardMixin } from '../../mixins/dialogBoard.js';
@@ -62,7 +62,7 @@ var script = {
62
62
  NcLoadingIcon,
63
63
  NcCheckboxRadioSwitch,
64
64
  Upload,
65
- ImportIcon: Import,
65
+ ImportIcon,
66
66
  ChevronDown,
67
67
  },
68
68
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnMassImportDialog.vue2.js","sources":["../../../../src/components/CnMassImportDialog/CnMassImportDialog.vue"],"sourcesContent":["<template>\n\t<CnDialog\n\t\t:look=\"look\"\n\t\t:width=\"width\"\n\t\tdefaultWidth=\"wizard\"\n\t\t:eyebrow=\"eyebrow\"\n\t\t:subtitle=\"subtitle\"\n\t\t:name=\"dialogTitle\"\n\t\tsize=\"large\"\n\t\t:noClose=\"loading\"\n\t\t@closing=\"$emit('close')\">\n\t\t<!-- Success/error messages -->\n\t\t<NcNoteCard v-if=\"result && result.success && !hasErrors\" type=\"success\">\n\t\t\t{{ successText }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard v-if=\"result && result.success && hasErrors\" type=\"warning\">\n\t\t\t{{ partialSuccessText }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard v-if=\"result && result.error\" type=\"error\">\n\t\t\t{{ result.error }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Results summary table -->\n\t\t<div v-if=\"result && result.summary\"\n\t\t\tclass=\"cn-mass-import__results\"\n\t\t\tdata-testid=\"cn-modal\"\n\t\t\tdata-testid-modal=\"cn-mass-import-dialog\"\n\t\t\tdata-testid-phase=\"result\">\n\t\t\t<h3>{{ summaryTitle }}</h3>\n\t\t\t<table class=\"cn-mass-import__summary-table\">\n\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th>{{ sheetLabel }}</th>\n\t\t\t\t\t\t<th>{{ foundLabel }}</th>\n\t\t\t\t\t\t<th>{{ createdLabel }}</th>\n\t\t\t\t\t\t<th>{{ updatedLabel }}</th>\n\t\t\t\t\t\t<th>{{ unchangedLabel }}</th>\n\t\t\t\t\t\t<th>{{ errorsLabel }}</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<template v-for=\"(sheet, key) in result.summary\" :key=\"key\">\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__sheet-name\">\n\t\t\t\t\t\t\t\t{{ key }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--found\">\n\t\t\t\t\t\t\t\t{{ sheet.found || 0 }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--created\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.created) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--updated\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.updated) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--unchanged\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.unchanged) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--errors\">\n\t\t\t\t\t\t\t\t<span>{{ getCount(sheet.errors) }}</span>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tv-if=\"getCount(sheet.errors) > 0\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-mass-import__expand\"\n\t\t\t\t\t\t\t\t\t:class=\"{ 'cn-mass-import__expand--open': expandedErrors[key] }\"\n\t\t\t\t\t\t\t\t\t@click=\"toggleErrors(key)\">\n\t\t\t\t\t\t\t\t\t<ChevronDown :size=\"16\" />\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<!-- Error details row -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-if=\"expandedErrors[key] && sheet.errors && sheet.errors.length\"\n\t\t\t\t\t\t\t:key=\"`${key}-errors`\"\n\t\t\t\t\t\t\tclass=\"cn-mass-import__error-row\">\n\t\t\t\t\t\t\t<td colspan=\"6\">\n\t\t\t\t\t\t\t\t<table class=\"cn-mass-import__error-table\">\n\t\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<th>Row</th>\n\t\t\t\t\t\t\t\t\t\t\t<th>Type</th>\n\t\t\t\t\t\t\t\t\t\t\t<th>Message</th>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t\t<tr v-for=\"(err, idx) in sheet.errors\" :key=\"idx\">\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.row || '—' }}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.type || 'Unknown' }}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.error || err.message || '—' }}</td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</template>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\n\t\t<!-- Upload form -->\n\t\t<div v-if=\"!result\"\n\t\t\tclass=\"cn-mass-import__form\"\n\t\t\tdata-testid=\"cn-modal\"\n\t\t\tdata-testid-modal=\"cn-mass-import-dialog\"\n\t\t\tdata-testid-phase=\"form\">\n\t\t\t<input\n\t\t\t\tref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"acceptedTypes\"\n\t\t\t\tstyle=\"display: none\"\n\t\t\t\t@change=\"handleFileSelect\">\n\n\t\t\t<div class=\"cn-mass-import__file-row\">\n\t\t\t\t<NcButton @click=\"$refs.fileInput.click()\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Upload :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ selectFileLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<div v-if=\"selectedFile\" class=\"cn-mass-import__file-info\">\n\t\t\t\t\t<span class=\"cn-mass-import__file-name\">{{ selectedFile.name }}</span>\n\t\t\t\t\t<span class=\"cn-mass-import__file-size\">({{ formatFileSize(selectedFile.size) }})</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Additional fields slot (e.g., register/schema selectors) -->\n\t\t\t<slot name=\"fields\" :file=\"selectedFile\" />\n\n\t\t\t<!-- File type help text -->\n\t\t\t<div v-if=\"fileTypeHelp\" class=\"cn-mass-import__help\">\n\t\t\t\t<p><strong>{{ supportedFormatsLabel }}</strong></p>\n\t\t\t\t<ul>\n\t\t\t\t\t<li v-for=\"(help, idx) in fileTypeHelp\" :key=\"idx\">\n\t\t\t\t\t\t<strong>{{ help.label }}</strong> — {{ help.description }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<!-- Import options -->\n\t\t\t<div v-if=\"options.length > 0\" class=\"cn-mass-import__options\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-for=\"opt in options\"\n\t\t\t\t\t:key=\"opt.key\"\n\t\t\t\t\t:modelValue=\"optionValues[opt.key]\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setOption(opt.key, $event)\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t\t<template v-if=\"opt.description\" #helper>\n\t\t\t\t\t\t{{ opt.description }}\n\t\t\t\t\t</template>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Loading indicator -->\n\t\t<NcNoteCard v-if=\"loading\" type=\"info\">\n\t\t\t{{ loadingText }}\n\t\t</NcNoteCard>\n\n\t\t<template #actions>\n\t\t\t<NcButton @click=\"$emit('close')\">\n\t\t\t\t{{ result ? closeLabel : cancelLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"!result\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"loading || !selectedFile || !canSubmit\"\n\t\t\t\t@click=\"executeImport\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"20\" />\n\t\t\t\t\t<ImportIcon v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ confirmLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ImportIcon from 'vue-material-design-icons/Import.vue'\nimport Upload from 'vue-material-design-icons/Upload.vue'\nimport CnDialog from '../CnDialog/CnDialog.vue'\nimport { dialogBoardMixin } from '../../mixins/dialogBoard.js'\n\n/**\n * CnMassImportDialog — File import dialog with options and results summary.\n *\n * Supports file upload (JSON, Excel, CSV), configurable import options via\n * toggle switches, and a results summary table showing created/updated/error\n * counts per sheet. Based on the OpenRegister ImportRegister pattern.\n *\n * The dialog does NOT perform the import itself — it emits a `confirm` event\n * with the file and options. The parent handles the API call and calls\n * `setResult()` via a ref.\n *\n * ```vue\n * <CnMassImportDialog\n * v-if=\"showImportDialog\"\n * ref=\"importDialog\"\n * :options=\"importOptions\"\n * @confirm=\"onImportConfirm\"\n * @close=\"showImportDialog = false\">\n * <template #fields=\"{ file }\">\n * <NcSelect v-if=\"file\" :options=\"schemas\" @update:model-value=\"selectedSchema = $event\" />\n * </template>\n * </CnMassImportDialog>\n * ```\n *\n * // In data:\n * importOptions: [\n * { key: 'validation', label: 'Enable validation', description: 'Validate against schema', default: true },\n * { key: 'publish', label: 'Auto-publish', description: 'Set published date', default: false },\n * ]\n *\n * // In methods:\n * async onImportConfirm({ file, options }) {\n * try {\n * const formData = new FormData()\n * formData.append('file', file)\n * const result = await axios.post('/api/import', formData, { params: options })\n * this.$refs.importDialog.setResult({ success: true, summary: result.data.summary })\n * } catch (e) {\n * this.$refs.importDialog.setResult({ error: e.message })\n * }\n * }\n *\n * @event confirm Emitted when the user clicks the import button. Payload: `{ file: File, options: Record<string, boolean> }`.\n * @event close Emitted when the dialog should be closed (cancel, close button, or auto-close after success).\n * @slot fields Custom form fields rendered between the file picker and the options list. Receives `{ file }` as the slot scope so the parent can react to file selection.\n */\nexport default {\n\tname: 'CnMassImportDialog',\n\n\tcomponents: {\n\t\tCnDialog,\n\t\tNcButton,\n\t\tNcNoteCard,\n\t\tNcLoadingIcon,\n\t\tNcCheckboxRadioSwitch,\n\t\tUpload,\n\t\tImportIcon,\n\t\tChevronDown,\n\t},\n\n\tmixins: [dialogBoardMixin],\n\n\tprops: {\n\t\t/** Dialog title */\n\t\tdialogTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import data'),\n\t\t},\n\n\t\t/** Accepted file types (input accept attribute) */\n\t\tacceptedTypes: {\n\t\t\ttype: String,\n\t\t\tdefault: '.json,.xlsx,.xls,.csv',\n\t\t},\n\n\t\t/** Import option definitions */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** File type help entries */\n\t\tfileTypeHelp: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [\n\t\t\t\t{ label: 'JSON', description: 'Configuration and objects.' },\n\t\t\t\t{ label: 'Excel (.xlsx, .xls)', description: 'Tabular objects data with multiple sheets.' },\n\t\t\t\t{ label: 'CSV', description: 'Single table of objects data.' },\n\t\t\t],\n\t\t},\n\n\t\t/** Whether the form is ready to submit (parent can control via slot logic) */\n\t\tcanSubmit: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Success text when all rows imported without errors */\n\t\tsuccessText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import completed successfully!'),\n\t\t},\n\n\t\t/** Text when import partially succeeded */\n\t\tpartialSuccessText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import completed with errors. Check the details below.'),\n\t\t},\n\n\t\t/** Text shown while importing */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Importing data — this may take a moment for large files…'),\n\t\t},\n\n\t\t/** Heading rendered above the per-sheet results table. */\n\t\tsummaryTitle: { type: String, default: () => t('nextcloud-vue', 'Import summary') },\n\t\t/** Label for the \"Supported file types\" intro line. */\n\t\tsupportedFormatsLabel: { type: String, default: () => t('nextcloud-vue', 'Supported file types:') },\n\t\t/** Label for the file-picker trigger button. */\n\t\tselectFileLabel: { type: String, default: () => t('nextcloud-vue', 'Select file') },\n\t\t/** Label for the cancel button (visible before the import runs). */\n\t\tcancelLabel: { type: String, default: () => t('nextcloud-vue', 'Cancel') },\n\t\t/** Label for the close button (visible after import completes). */\n\t\tcloseLabel: { type: String, default: () => t('nextcloud-vue', 'Close') },\n\t\t/** Label for the primary confirm button that triggers the import. */\n\t\tconfirmLabel: { type: String, default: () => t('nextcloud-vue', 'Import') },\n\t\t/** Column header for the per-sheet results row label. */\n\t\tsheetLabel: { type: String, default: () => t('nextcloud-vue', 'Sheet') },\n\t\t/** Column header for the \"rows found\" summary count. */\n\t\tfoundLabel: { type: String, default: () => t('nextcloud-vue', 'Found') },\n\t\t/** Column header for the \"rows created\" summary count. */\n\t\tcreatedLabel: { type: String, default: () => t('nextcloud-vue', 'Created') },\n\t\t/** Column header for the \"rows updated\" summary count. */\n\t\tupdatedLabel: { type: String, default: () => t('nextcloud-vue', 'Updated') },\n\t\t/** Column header for the \"rows unchanged\" summary count. */\n\t\tunchangedLabel: { type: String, default: () => t('nextcloud-vue', 'Unchanged') },\n\t\t/** Column header / collapsible toggle for the per-sheet errors list. */\n\t\terrorsLabel: { type: String, default: () => t('nextcloud-vue', 'Errors') },\n\t},\n\n\temits: ['close', 'confirm'],\n\n\tdata() {\n\t\tconst optionValues = {}\n\t\tthis.options.forEach((opt) => {\n\t\t\toptionValues[opt.key] = opt.default !== undefined ? opt.default : false\n\t\t})\n\t\treturn {\n\t\t\tselectedFile: null,\n\t\t\tloading: false,\n\t\t\tresult: null,\n\t\t\toptionValues,\n\t\t\texpandedErrors: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\thasErrors() {\n\t\t\tif (!this.result || !this.result.summary) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Object.values(this.result.summary).some((sheet) => sheet.errors && sheet.errors.length > 0)\n\t\t},\n\t},\n\n\tmethods: {\n\t\thandleFileSelect(event) {\n\t\t\tconst file = event.target.files[0]\n\t\t\tthis.selectedFile = file || null\n\t\t},\n\n\t\tsetOption(key, value) {\n\t\t\tthis.optionValues[key] = value\n\t\t},\n\n\t\tformatFileSize(bytes) {\n\t\t\tif (bytes === 0) {\n\t\t\t\treturn '0 B'\n\t\t\t}\n\t\t\tconst k = 1024\n\t\t\tconst sizes = ['B', 'KB', 'MB', 'GB']\n\t\t\tconst i = Math.floor(Math.log(bytes) / Math.log(k))\n\t\t\treturn parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]\n\t\t},\n\n\t\tgetCount(val) {\n\t\t\tif (Array.isArray(val)) {\n\t\t\t\treturn val.length\n\t\t\t}\n\t\t\tif (typeof val === 'number') {\n\t\t\t\treturn val\n\t\t\t}\n\t\t\treturn 0\n\t\t},\n\n\t\ttoggleErrors(key) {\n\t\t\tthis.expandedErrors[key] = !this.expandedErrors[key]\n\t\t},\n\n\t\texecuteImport() {\n\t\t\tthis.loading = true\n\t\t\tthis.$emit('confirm', {\n\t\t\t\tfile: this.selectedFile,\n\t\t\t\toptions: { ...this.optionValues },\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Set the result of the import operation.\n\t\t *\n\t\t * @param {{ success?: boolean, error?: string, summary?: object }} resultData - Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetResult(resultData) {\n\t\t\tthis.loading = false\n\t\t\tthis.result = resultData\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-mass-import__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.cn-mass-import__file-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-mass-import__file-info {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n}\n\n.cn-mass-import__file-name {\n\tfont-weight: 500;\n}\n\n.cn-mass-import__file-size {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-mass-import__help {\n\tpadding: 12px;\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-mass-import__help p {\n\tmargin: 0 0 8px;\n}\n\n.cn-mass-import__help ul {\n\tmargin: 0;\n\tpadding-left: 20px;\n}\n\n.cn-mass-import__options {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n/* Summary table */\n.cn-mass-import__results h3 {\n\tmargin: 0 0 12px;\n\tfont-size: 1.1rem;\n}\n\n.cn-mass-import__summary-table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n.cn-mass-import__summary-table th,\n.cn-mass-import__summary-table td {\n\tpadding: 8px 12px;\n\ttext-align: left;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-mass-import__summary-table th {\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n\tfont-size: 0.85rem;\n\ttext-transform: uppercase;\n}\n\n.cn-mass-import__sheet-name {\n\tfont-weight: 600;\n}\n\n.cn-mass-import__stat {\n\ttext-align: center;\n\tfont-weight: 600;\n}\n\n.cn-mass-import__stat--found { color: var(--color-primary-element); }\n\n.cn-mass-import__stat--created { color: var(--color-success); }\n\n.cn-mass-import__stat--updated { color: var(--color-warning); }\n\n.cn-mass-import__stat--unchanged { color: var(--color-text-maxcontrast); }\n\n.cn-mass-import__stat--errors { color: var(--color-error); }\n\n.cn-mass-import__expand {\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 2px;\n\tborder-radius: var(--border-radius-small, 4px);\n\tcolor: var(--color-error);\n\ttransition: transform 0.2s ease;\n\tvertical-align: middle;\n\tmargin-left: 4px;\n}\n\n.cn-mass-import__expand:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-mass-import__expand--open {\n\ttransform: rotate(180deg);\n}\n\n/* Error details */\n.cn-mass-import__error-row td {\n\tpadding: 8px 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-mass-import__error-table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 0.9em;\n}\n\n.cn-mass-import__error-table th,\n.cn-mass-import__error-table td {\n\tpadding: 6px 10px;\n\ttext-align: left;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-mass-import__error-table th {\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n\tfont-size: 0.85rem;\n}\n</style>\n"],"names":["CnDialog","ImportIcon","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnMassImportDialog.vue2.js","sources":["../../../../src/components/CnMassImportDialog/CnMassImportDialog.vue"],"sourcesContent":["<template>\n\t<CnDialog\n\t\t:look=\"look\"\n\t\t:width=\"width\"\n\t\tdefaultWidth=\"wizard\"\n\t\t:eyebrow=\"eyebrow\"\n\t\t:subtitle=\"subtitle\"\n\t\t:name=\"dialogTitle\"\n\t\tsize=\"large\"\n\t\t:noClose=\"loading\"\n\t\t@closing=\"$emit('close')\">\n\t\t<!-- Success/error messages -->\n\t\t<NcNoteCard v-if=\"result && result.success && !hasErrors\" type=\"success\">\n\t\t\t{{ successText }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard v-if=\"result && result.success && hasErrors\" type=\"warning\">\n\t\t\t{{ partialSuccessText }}\n\t\t</NcNoteCard>\n\t\t<NcNoteCard v-if=\"result && result.error\" type=\"error\">\n\t\t\t{{ result.error }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Results summary table -->\n\t\t<div v-if=\"result && result.summary\"\n\t\t\tclass=\"cn-mass-import__results\"\n\t\t\tdata-testid=\"cn-modal\"\n\t\t\tdata-testid-modal=\"cn-mass-import-dialog\"\n\t\t\tdata-testid-phase=\"result\">\n\t\t\t<h3>{{ summaryTitle }}</h3>\n\t\t\t<table class=\"cn-mass-import__summary-table\">\n\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th>{{ sheetLabel }}</th>\n\t\t\t\t\t\t<th>{{ foundLabel }}</th>\n\t\t\t\t\t\t<th>{{ createdLabel }}</th>\n\t\t\t\t\t\t<th>{{ updatedLabel }}</th>\n\t\t\t\t\t\t<th>{{ unchangedLabel }}</th>\n\t\t\t\t\t\t<th>{{ errorsLabel }}</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<template v-for=\"(sheet, key) in result.summary\" :key=\"key\">\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__sheet-name\">\n\t\t\t\t\t\t\t\t{{ key }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--found\">\n\t\t\t\t\t\t\t\t{{ sheet.found || 0 }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--created\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.created) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--updated\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.updated) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--unchanged\">\n\t\t\t\t\t\t\t\t{{ getCount(sheet.unchanged) }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td class=\"cn-mass-import__stat cn-mass-import__stat--errors\">\n\t\t\t\t\t\t\t\t<span>{{ getCount(sheet.errors) }}</span>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tv-if=\"getCount(sheet.errors) > 0\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-mass-import__expand\"\n\t\t\t\t\t\t\t\t\t:class=\"{ 'cn-mass-import__expand--open': expandedErrors[key] }\"\n\t\t\t\t\t\t\t\t\t@click=\"toggleErrors(key)\">\n\t\t\t\t\t\t\t\t\t<ChevronDown :size=\"16\" />\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<!-- Error details row -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-if=\"expandedErrors[key] && sheet.errors && sheet.errors.length\"\n\t\t\t\t\t\t\t:key=\"`${key}-errors`\"\n\t\t\t\t\t\t\tclass=\"cn-mass-import__error-row\">\n\t\t\t\t\t\t\t<td colspan=\"6\">\n\t\t\t\t\t\t\t\t<table class=\"cn-mass-import__error-table\">\n\t\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<th>Row</th>\n\t\t\t\t\t\t\t\t\t\t\t<th>Type</th>\n\t\t\t\t\t\t\t\t\t\t\t<th>Message</th>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t\t<tr v-for=\"(err, idx) in sheet.errors\" :key=\"idx\">\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.row || '—' }}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.type || 'Unknown' }}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ err.error || err.message || '—' }}</td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</template>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\n\t\t<!-- Upload form -->\n\t\t<div v-if=\"!result\"\n\t\t\tclass=\"cn-mass-import__form\"\n\t\t\tdata-testid=\"cn-modal\"\n\t\t\tdata-testid-modal=\"cn-mass-import-dialog\"\n\t\t\tdata-testid-phase=\"form\">\n\t\t\t<input\n\t\t\t\tref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"acceptedTypes\"\n\t\t\t\tstyle=\"display: none\"\n\t\t\t\t@change=\"handleFileSelect\">\n\n\t\t\t<div class=\"cn-mass-import__file-row\">\n\t\t\t\t<NcButton @click=\"$refs.fileInput.click()\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Upload :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ selectFileLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<div v-if=\"selectedFile\" class=\"cn-mass-import__file-info\">\n\t\t\t\t\t<span class=\"cn-mass-import__file-name\">{{ selectedFile.name }}</span>\n\t\t\t\t\t<span class=\"cn-mass-import__file-size\">({{ formatFileSize(selectedFile.size) }})</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Additional fields slot (e.g., register/schema selectors) -->\n\t\t\t<slot name=\"fields\" :file=\"selectedFile\" />\n\n\t\t\t<!-- File type help text -->\n\t\t\t<div v-if=\"fileTypeHelp\" class=\"cn-mass-import__help\">\n\t\t\t\t<p><strong>{{ supportedFormatsLabel }}</strong></p>\n\t\t\t\t<ul>\n\t\t\t\t\t<li v-for=\"(help, idx) in fileTypeHelp\" :key=\"idx\">\n\t\t\t\t\t\t<strong>{{ help.label }}</strong> — {{ help.description }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<!-- Import options -->\n\t\t\t<div v-if=\"options.length > 0\" class=\"cn-mass-import__options\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-for=\"opt in options\"\n\t\t\t\t\t:key=\"opt.key\"\n\t\t\t\t\t:modelValue=\"optionValues[opt.key]\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t@update:modelValue=\"setOption(opt.key, $event)\">\n\t\t\t\t\t{{ opt.label }}\n\t\t\t\t\t<template v-if=\"opt.description\" #helper>\n\t\t\t\t\t\t{{ opt.description }}\n\t\t\t\t\t</template>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Loading indicator -->\n\t\t<NcNoteCard v-if=\"loading\" type=\"info\">\n\t\t\t{{ loadingText }}\n\t\t</NcNoteCard>\n\n\t\t<template #actions>\n\t\t\t<NcButton @click=\"$emit('close')\">\n\t\t\t\t{{ result ? closeLabel : cancelLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"!result\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"loading || !selectedFile || !canSubmit\"\n\t\t\t\t@click=\"executeImport\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"20\" />\n\t\t\t\t\t<ImportIcon v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ confirmLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ImportIcon from 'vue-material-design-icons/Import.vue'\nimport Upload from 'vue-material-design-icons/Upload.vue'\nimport CnDialog from '../CnDialog/CnDialog.vue'\nimport { dialogBoardMixin } from '../../mixins/dialogBoard.js'\n\n/**\n * CnMassImportDialog — File import dialog with options and results summary.\n *\n * Supports file upload (JSON, Excel, CSV), configurable import options via\n * toggle switches, and a results summary table showing created/updated/error\n * counts per sheet. Based on the OpenRegister ImportRegister pattern.\n *\n * The dialog does NOT perform the import itself — it emits a `confirm` event\n * with the file and options. The parent handles the API call and calls\n * `setResult()` via a ref.\n *\n * ```vue\n * <CnMassImportDialog\n * v-if=\"showImportDialog\"\n * ref=\"importDialog\"\n * :options=\"importOptions\"\n * @confirm=\"onImportConfirm\"\n * @close=\"showImportDialog = false\">\n * <template #fields=\"{ file }\">\n * <NcSelect v-if=\"file\" :options=\"schemas\" @update:model-value=\"selectedSchema = $event\" />\n * </template>\n * </CnMassImportDialog>\n * ```\n *\n * // In data:\n * importOptions: [\n * { key: 'validation', label: 'Enable validation', description: 'Validate against schema', default: true },\n * { key: 'publish', label: 'Auto-publish', description: 'Set published date', default: false },\n * ]\n *\n * // In methods:\n * async onImportConfirm({ file, options }) {\n * try {\n * const formData = new FormData()\n * formData.append('file', file)\n * const result = await axios.post('/api/import', formData, { params: options })\n * this.$refs.importDialog.setResult({ success: true, summary: result.data.summary })\n * } catch (e) {\n * this.$refs.importDialog.setResult({ error: e.message })\n * }\n * }\n *\n * @event confirm Emitted when the user clicks the import button. Payload: `{ file: File, options: Record<string, boolean> }`.\n * @event close Emitted when the dialog should be closed (cancel, close button, or auto-close after success).\n * @slot fields Custom form fields rendered between the file picker and the options list. Receives `{ file }` as the slot scope so the parent can react to file selection.\n */\nexport default {\n\tname: 'CnMassImportDialog',\n\n\tcomponents: {\n\t\tCnDialog,\n\t\tNcButton,\n\t\tNcNoteCard,\n\t\tNcLoadingIcon,\n\t\tNcCheckboxRadioSwitch,\n\t\tUpload,\n\t\tImportIcon,\n\t\tChevronDown,\n\t},\n\n\tmixins: [dialogBoardMixin],\n\n\tprops: {\n\t\t/** Dialog title */\n\t\tdialogTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import data'),\n\t\t},\n\n\t\t/** Accepted file types (input accept attribute) */\n\t\tacceptedTypes: {\n\t\t\ttype: String,\n\t\t\tdefault: '.json,.xlsx,.xls,.csv',\n\t\t},\n\n\t\t/** Import option definitions */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** File type help entries */\n\t\tfileTypeHelp: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [\n\t\t\t\t{ label: 'JSON', description: 'Configuration and objects.' },\n\t\t\t\t{ label: 'Excel (.xlsx, .xls)', description: 'Tabular objects data with multiple sheets.' },\n\t\t\t\t{ label: 'CSV', description: 'Single table of objects data.' },\n\t\t\t],\n\t\t},\n\n\t\t/** Whether the form is ready to submit (parent can control via slot logic) */\n\t\tcanSubmit: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Success text when all rows imported without errors */\n\t\tsuccessText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import completed successfully!'),\n\t\t},\n\n\t\t/** Text when import partially succeeded */\n\t\tpartialSuccessText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Import completed with errors. Check the details below.'),\n\t\t},\n\n\t\t/** Text shown while importing */\n\t\tloadingText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Importing data — this may take a moment for large files…'),\n\t\t},\n\n\t\t/** Heading rendered above the per-sheet results table. */\n\t\tsummaryTitle: { type: String, default: () => t('nextcloud-vue', 'Import summary') },\n\t\t/** Label for the \"Supported file types\" intro line. */\n\t\tsupportedFormatsLabel: { type: String, default: () => t('nextcloud-vue', 'Supported file types:') },\n\t\t/** Label for the file-picker trigger button. */\n\t\tselectFileLabel: { type: String, default: () => t('nextcloud-vue', 'Select file') },\n\t\t/** Label for the cancel button (visible before the import runs). */\n\t\tcancelLabel: { type: String, default: () => t('nextcloud-vue', 'Cancel') },\n\t\t/** Label for the close button (visible after import completes). */\n\t\tcloseLabel: { type: String, default: () => t('nextcloud-vue', 'Close') },\n\t\t/** Label for the primary confirm button that triggers the import. */\n\t\tconfirmLabel: { type: String, default: () => t('nextcloud-vue', 'Import') },\n\t\t/** Column header for the per-sheet results row label. */\n\t\tsheetLabel: { type: String, default: () => t('nextcloud-vue', 'Sheet') },\n\t\t/** Column header for the \"rows found\" summary count. */\n\t\tfoundLabel: { type: String, default: () => t('nextcloud-vue', 'Found') },\n\t\t/** Column header for the \"rows created\" summary count. */\n\t\tcreatedLabel: { type: String, default: () => t('nextcloud-vue', 'Created') },\n\t\t/** Column header for the \"rows updated\" summary count. */\n\t\tupdatedLabel: { type: String, default: () => t('nextcloud-vue', 'Updated') },\n\t\t/** Column header for the \"rows unchanged\" summary count. */\n\t\tunchangedLabel: { type: String, default: () => t('nextcloud-vue', 'Unchanged') },\n\t\t/** Column header / collapsible toggle for the per-sheet errors list. */\n\t\terrorsLabel: { type: String, default: () => t('nextcloud-vue', 'Errors') },\n\t},\n\n\temits: ['close', 'confirm'],\n\n\tdata() {\n\t\tconst optionValues = {}\n\t\tthis.options.forEach((opt) => {\n\t\t\toptionValues[opt.key] = opt.default !== undefined ? opt.default : false\n\t\t})\n\t\treturn {\n\t\t\tselectedFile: null,\n\t\t\tloading: false,\n\t\t\tresult: null,\n\t\t\toptionValues,\n\t\t\texpandedErrors: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\thasErrors() {\n\t\t\tif (!this.result || !this.result.summary) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Object.values(this.result.summary).some((sheet) => sheet.errors && sheet.errors.length > 0)\n\t\t},\n\t},\n\n\tmethods: {\n\t\thandleFileSelect(event) {\n\t\t\tconst file = event.target.files[0]\n\t\t\tthis.selectedFile = file || null\n\t\t},\n\n\t\tsetOption(key, value) {\n\t\t\tthis.optionValues[key] = value\n\t\t},\n\n\t\tformatFileSize(bytes) {\n\t\t\tif (bytes === 0) {\n\t\t\t\treturn '0 B'\n\t\t\t}\n\t\t\tconst k = 1024\n\t\t\tconst sizes = ['B', 'KB', 'MB', 'GB']\n\t\t\tconst i = Math.floor(Math.log(bytes) / Math.log(k))\n\t\t\treturn parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]\n\t\t},\n\n\t\tgetCount(val) {\n\t\t\tif (Array.isArray(val)) {\n\t\t\t\treturn val.length\n\t\t\t}\n\t\t\tif (typeof val === 'number') {\n\t\t\t\treturn val\n\t\t\t}\n\t\t\treturn 0\n\t\t},\n\n\t\ttoggleErrors(key) {\n\t\t\tthis.expandedErrors[key] = !this.expandedErrors[key]\n\t\t},\n\n\t\texecuteImport() {\n\t\t\tthis.loading = true\n\t\t\tthis.$emit('confirm', {\n\t\t\t\tfile: this.selectedFile,\n\t\t\t\toptions: { ...this.optionValues },\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Set the result of the import operation.\n\t\t *\n\t\t * @param {{ success?: boolean, error?: string, summary?: object }} resultData - Result data to pass to the dialog\n\t\t * @public\n\t\t */\n\t\tsetResult(resultData) {\n\t\t\tthis.loading = false\n\t\t\tthis.result = resultData\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-mass-import__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n}\n\n.cn-mass-import__file-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-mass-import__file-info {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n}\n\n.cn-mass-import__file-name {\n\tfont-weight: 500;\n}\n\n.cn-mass-import__file-size {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-mass-import__help {\n\tpadding: 12px;\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-mass-import__help p {\n\tmargin: 0 0 8px;\n}\n\n.cn-mass-import__help ul {\n\tmargin: 0;\n\tpadding-left: 20px;\n}\n\n.cn-mass-import__options {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n/* Summary table */\n.cn-mass-import__results h3 {\n\tmargin: 0 0 12px;\n\tfont-size: 1.1rem;\n}\n\n.cn-mass-import__summary-table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tborder-radius: var(--border-radius);\n\toverflow: hidden;\n\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n\n.cn-mass-import__summary-table th,\n.cn-mass-import__summary-table td {\n\tpadding: 8px 12px;\n\ttext-align: left;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-mass-import__summary-table th {\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n\tfont-size: 0.85rem;\n\ttext-transform: uppercase;\n}\n\n.cn-mass-import__sheet-name {\n\tfont-weight: 600;\n}\n\n.cn-mass-import__stat {\n\ttext-align: center;\n\tfont-weight: 600;\n}\n\n.cn-mass-import__stat--found { color: var(--color-primary-element); }\n\n.cn-mass-import__stat--created { color: var(--color-success); }\n\n.cn-mass-import__stat--updated { color: var(--color-warning); }\n\n.cn-mass-import__stat--unchanged { color: var(--color-text-maxcontrast); }\n\n.cn-mass-import__stat--errors { color: var(--color-error); }\n\n.cn-mass-import__expand {\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 2px;\n\tborder-radius: var(--border-radius-small, 4px);\n\tcolor: var(--color-error);\n\ttransition: transform 0.2s ease;\n\tvertical-align: middle;\n\tmargin-left: 4px;\n}\n\n.cn-mass-import__expand:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-mass-import__expand--open {\n\ttransform: rotate(180deg);\n}\n\n/* Error details */\n.cn-mass-import__error-row td {\n\tpadding: 8px 12px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-mass-import__error-table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 0.9em;\n}\n\n.cn-mass-import__error-table th,\n.cn-mass-import__error-table td {\n\tpadding: 6px 10px;\n\ttext-align: left;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-mass-import__error-table th {\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n\tfont-size: 0.85rem;\n}\n</style>\n"],"names":["CnDialog","t"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnRadioCardsWidget.vue2.js';
2
2
  import { render } from './CnRadioCardsWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-3b685691";
5
+ script.__scopeId = "data-v-8ac292d6";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -13,7 +13,7 @@ let uid = 0;
13
13
  * underneath, so the choice is perceivable without colour and works from the
14
14
  * keyboard.
15
15
  *
16
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
16
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md
17
17
  */
18
18
  var script = {
19
19
  name: 'CnRadioCardsWidget',
@@ -33,6 +33,7 @@ var script = {
33
33
  * - `options` (required) — `[{ value, label, description? }]`.
34
34
  * - `label` — a legend over the cards (visually shown).
35
35
  * - `columns` — 1, 2 or 3 (default 2).
36
+ * - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.
36
37
  * - `readOnly`.
37
38
  *
38
39
  * @type {object}
@@ -94,7 +95,20 @@ var script = {
94
95
  return this.tr(this.content.label || '')
95
96
  },
96
97
 
98
+ /**
99
+ * Whether the options draw as a compact list (`variant: "list"`).
100
+ *
101
+ * @return {boolean}
102
+ * @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list
103
+ */
104
+ isList() {
105
+ return this.content.variant === 'list'
106
+ },
107
+
97
108
  columns() {
109
+ if (this.isList) {
110
+ return 1
111
+ }
98
112
  return [1, 3].includes(this.content.columns) ? this.content.columns : 2
99
113
  },
100
114
 
@@ -146,7 +160,7 @@ var script = {
146
160
  *
147
161
  * @param {object} option The chosen option.
148
162
  * @return {Promise<void>}
149
- * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
163
+ * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
150
164
  */
151
165
  async choose(option) {
152
166
  if ((!this.objectUrl && !this.hasSave) || !this.content.field) {
@@ -1 +1 @@
1
- {"version":3,"file":"CnRadioCardsWidget.vue2.js","sources":["../../../../src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<fieldset class=\"cn-radio-cards\" data-testid=\"cn-radio-cards\" :aria-busy=\"busy ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-radio-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\t\t<div class=\"cn-radio-cards__grid\" :class=\"`cn-radio-cards__grid--${columns}`\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in options\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-radio-cards__card\"\n\t\t\t\t:class=\"{ 'cn-radio-cards__card--selected': isSelected(option) }\"\n\t\t\t\t:data-testid=\"`cn-radio-cards-option-${option.value}`\">\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-radio-cards__input\"\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"readOnly || busy\"\n\t\t\t\t\t@change=\"choose(option)\">\n\t\t\t\t<span class=\"cn-radio-cards__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span class=\"cn-radio-cards__text\">\n\t\t\t\t\t<span class=\"cn-radio-cards__title\">{{ option.label }}</span>\n\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-radio-cards__description\">{{ option.description }}</span>\n\t\t\t\t</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<NcNoteCard v-if=\"notice\" type=\"error\" data-testid=\"cn-radio-cards-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</fieldset>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\nlet uid = 0\n\n/**\n * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,\n * each with a title and a description, bound to an enum field of the CURRENT\n * object. Choosing a card saves at once with ONE PATCH of that field; a refused\n * PATCH puts the choice back and shows the server's words. Real radio inputs\n * underneath, so the choice is perceivable without colour and works from the\n * keyboard.\n *\n * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md\n */\nexport default {\n\tname: 'CnRadioCardsWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the enum field, a top-level name or a dot-path.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `options` (required) — `[{ value, label, description? }]`.\n\t\t * - `label` — a legend over the cards (visually shown).\n\t\t * - `columns` — 1, 2 or 3 (default 2).\n\t\t * - `readOnly`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\tuid += 1\n\t\treturn {\n\t\t\tgroupName: `cn-radio-cards-${uid}`,\n\t\t\tsaved: '',\n\t\t\tvalue: '',\n\t\t\tbusy: false,\n\t\t\tnotice: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\toptions() {\n\t\t\tconst list = Array.isArray(this.content.options) ? this.content.options : []\n\t\t\treturn list\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => (typeof option === 'object'\n\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }\n\t\t\t\t\t: { value: option, label: this.tr(String(option)), description: '' }))\n\t\t},\n\n\t\tlabel() {\n\t\t\treturn this.tr(this.content.label || '')\n\t\t},\n\n\t\tcolumns() {\n\t\t\treturn [1, 3].includes(this.content.columns) ? this.content.columns : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether the widget writes through the app's own endpoint (`content.save`). */\n\t\thasSave() {\n\t\t\treturn !!(this.content.save && this.content.save.url)\n\t\t},\n\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/** Read the choice off the object. */\n\t\tadopt() {\n\t\t\tconst stored = readPath(this.objectData || {}, this.content.field)\n\t\t\tthis.saved = stored === undefined || stored === null ? '' : String(stored)\n\t\t\tthis.value = this.saved\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.value === String(option.value)\n\t\t},\n\n\t\t/**\n\t\t * Choose a card and save that one field.\n\t\t *\n\t\t * @param {object} option The chosen option.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field\n\t\t */\n\t\tasync choose(option) {\n\t\t\tif ((!this.objectUrl && !this.hasSave) || !this.content.field) {\n\t\t\t\tthis.notice = this.tr('This choice cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.notice = ''\n\t\t\tthis.busy = true\n\t\t\tthis.value = String(option.value)\n\t\t\ttry {\n\t\t\t\tconst data = await writeFields(this.objectUrl, { [this.content.field]: option.value }, { save: this.content.save, object: this.objectData })\n\t\t\t\tconst written = readPath(data, this.content.field)\n\t\t\t\tthis.saved = String(written !== undefined && written !== null ? written : option.value)\n\t\t\t\tthis.value = this.saved\n\t\t\t\t/**\n\t\t\t\t * @event saved The choice was written.\n\t\t\t\t * @type {{field: string, value: unknown}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: option.value })\n\t\t\t} catch (e) {\n\t\t\t\tthis.value = this.saved\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.busy = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-radio-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-width: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n}\n\n.cn-radio-cards__legend {\n\tpadding: 0;\n\tmargin-bottom: 6px;\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__grid {\n\tdisplay: grid;\n\tgap: 10px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-radio-cards__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-radio-cards__grid--3 {\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.cn-radio-cards__card {\n\tposition: relative;\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 10px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-radio-cards__card--selected {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n\tbox-shadow: 0 0 0 1px var(--color-primary-element);\n}\n\n.cn-radio-cards__card:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */\n.cn-radio-cards__input {\n\tposition: absolute;\n\topacity: 0;\n\tinset: 0;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n.cn-radio-cards__dot {\n\tflex: 0 0 auto;\n\twidth: 14px;\n\theight: 14px;\n\tmargin-top: 3px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: 50%;\n\tbackground: var(--color-main-background);\n}\n\n.cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder: 4px solid var(--color-primary-element);\n}\n\n.cn-radio-cards__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-radio-cards__title {\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__description {\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n@media (max-width: 640px) {\n\t.cn-radio-cards__grid--2,\n\t.cn-radio-cards__grid--3 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnRadioCardsWidget.vue2.js","sources":["../../../../src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<fieldset class=\"cn-radio-cards\"\n\t\t:class=\"{ 'cn-radio-cards--list': isList }\"\n\t\tdata-testid=\"cn-radio-cards\"\n\t\t:aria-busy=\"busy ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-radio-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\t\t<div class=\"cn-radio-cards__grid\" :class=\"`cn-radio-cards__grid--${columns}`\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in options\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-radio-cards__card\"\n\t\t\t\t:class=\"{ 'cn-radio-cards__card--selected': isSelected(option) }\"\n\t\t\t\t:data-testid=\"`cn-radio-cards-option-${option.value}`\">\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-radio-cards__input\"\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"readOnly || busy\"\n\t\t\t\t\t@change=\"choose(option)\">\n\t\t\t\t<span class=\"cn-radio-cards__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span class=\"cn-radio-cards__text\">\n\t\t\t\t\t<span class=\"cn-radio-cards__title\">{{ option.label }}</span>\n\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-radio-cards__description\">{{ option.description }}</span>\n\t\t\t\t</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<NcNoteCard v-if=\"notice\" type=\"error\" data-testid=\"cn-radio-cards-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</fieldset>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\nlet uid = 0\n\n/**\n * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,\n * each with a title and a description, bound to an enum field of the CURRENT\n * object. Choosing a card saves at once with ONE PATCH of that field; a refused\n * PATCH puts the choice back and shows the server's words. Real radio inputs\n * underneath, so the choice is perceivable without colour and works from the\n * keyboard.\n *\n * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md\n */\nexport default {\n\tname: 'CnRadioCardsWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the enum field, a top-level name or a dot-path.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `options` (required) — `[{ value, label, description? }]`.\n\t\t * - `label` — a legend over the cards (visually shown).\n\t\t * - `columns` — 1, 2 or 3 (default 2).\n\t\t * - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.\n\t\t * - `readOnly`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\tuid += 1\n\t\treturn {\n\t\t\tgroupName: `cn-radio-cards-${uid}`,\n\t\t\tsaved: '',\n\t\t\tvalue: '',\n\t\t\tbusy: false,\n\t\t\tnotice: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\toptions() {\n\t\t\tconst list = Array.isArray(this.content.options) ? this.content.options : []\n\t\t\treturn list\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => (typeof option === 'object'\n\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }\n\t\t\t\t\t: { value: option, label: this.tr(String(option)), description: '' }))\n\t\t},\n\n\t\tlabel() {\n\t\t\treturn this.tr(this.content.label || '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the options draw as a compact list (`variant: \"list\"`).\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list\n\t\t */\n\t\tisList() {\n\t\t\treturn this.content.variant === 'list'\n\t\t},\n\n\t\tcolumns() {\n\t\t\tif (this.isList) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn [1, 3].includes(this.content.columns) ? this.content.columns : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether the widget writes through the app's own endpoint (`content.save`). */\n\t\thasSave() {\n\t\t\treturn !!(this.content.save && this.content.save.url)\n\t\t},\n\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/** Read the choice off the object. */\n\t\tadopt() {\n\t\t\tconst stored = readPath(this.objectData || {}, this.content.field)\n\t\t\tthis.saved = stored === undefined || stored === null ? '' : String(stored)\n\t\t\tthis.value = this.saved\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.value === String(option.value)\n\t\t},\n\n\t\t/**\n\t\t * Choose a card and save that one field.\n\t\t *\n\t\t * @param {object} option The chosen option.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field\n\t\t */\n\t\tasync choose(option) {\n\t\t\tif ((!this.objectUrl && !this.hasSave) || !this.content.field) {\n\t\t\t\tthis.notice = this.tr('This choice cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.notice = ''\n\t\t\tthis.busy = true\n\t\t\tthis.value = String(option.value)\n\t\t\ttry {\n\t\t\t\tconst data = await writeFields(this.objectUrl, { [this.content.field]: option.value }, { save: this.content.save, object: this.objectData })\n\t\t\t\tconst written = readPath(data, this.content.field)\n\t\t\t\tthis.saved = String(written !== undefined && written !== null ? written : option.value)\n\t\t\t\tthis.value = this.saved\n\t\t\t\t/**\n\t\t\t\t * @event saved The choice was written.\n\t\t\t\t * @type {{field: string, value: unknown}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: option.value })\n\t\t\t} catch (e) {\n\t\t\t\tthis.value = this.saved\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.busy = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-radio-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-width: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n}\n\n.cn-radio-cards__legend {\n\tpadding: 0;\n\tmargin-bottom: 6px;\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__grid {\n\tdisplay: grid;\n\tgap: 10px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-radio-cards__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-radio-cards__grid--3 {\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.cn-radio-cards__card {\n\tposition: relative;\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 10px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-radio-cards__card--selected {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n\tbox-shadow: 0 0 0 1px var(--color-primary-element);\n}\n\n.cn-radio-cards__card:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */\n.cn-radio-cards__input {\n\tposition: absolute;\n\topacity: 0;\n\tinset: 0;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n.cn-radio-cards__dot {\n\tflex: 0 0 auto;\n\twidth: 14px;\n\theight: 14px;\n\tmargin-top: 3px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: 50%;\n\tbackground: var(--color-main-background);\n}\n\n.cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder: 4px solid var(--color-primary-element);\n}\n\n.cn-radio-cards__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-radio-cards__title {\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__description {\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* `variant: \"list\"`: one radio button per line, the description under the\n label, no card around each (board DcBeheer, \"Soort organisatie\"). */\n.cn-radio-cards--list .cn-radio-cards__grid {\n\tgap: 8px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__card,\n.cn-radio-cards--list .cn-radio-cards__card--selected {\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n\tbox-shadow: none;\n\tfont-size: 15px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__dot {\n\twidth: 18px;\n\theight: 18px;\n\tmargin-top: 2px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder-width: 5px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__title {\n\tfont-weight: 400;\n}\n\n.cn-radio-cards--list .cn-radio-cards__description {\n\tfont-size: 13px;\n}\n\n@media (max-width: 640px) {\n\t.cn-radio-cards__grid--2,\n\t.cn-radio-cards__grid--3 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createElementVNode, normalizeClass, Fragment, renderList, createBlock, withCtx, createTextVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, toDisplayString, createCommentVNode, createElementVNode, Fragment, renderList, createBlock, withCtx, createTextVNode } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["aria-busy"];
4
4
  const _hoisted_2 = {
@@ -18,7 +18,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
18
18
  const _component_NcNoteCard = resolveComponent("NcNoteCard");
19
19
 
20
20
  return (openBlock(), createElementBlock("fieldset", {
21
- class: "cn-radio-cards",
21
+ class: normalizeClass(["cn-radio-cards", { 'cn-radio-cards--list': $options.isList }]),
22
22
  "data-testid": "cn-radio-cards",
23
23
  "aria-busy": $data.busy ? 'true' : 'false'
24
24
  }, [
@@ -68,7 +68,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
68
68
  _: 1 /* STABLE */
69
69
  }))
70
70
  : createCommentVNode("v-if", true)
71
- ], 8 /* PROPS */, _hoisted_1))
71
+ ], 10 /* CLASS, PROPS */, _hoisted_1))
72
72
  }
73
73
 
74
74
  export { render };
@@ -1 +1 @@
1
- {"version":3,"file":"CnRadioCardsWidget.vue3.js","sources":["../../../../src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue?vue&type=template&id=3b685691&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<fieldset class=\"cn-radio-cards\" data-testid=\"cn-radio-cards\" :aria-busy=\"busy ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-radio-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\t\t<div class=\"cn-radio-cards__grid\" :class=\"`cn-radio-cards__grid--${columns}`\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in options\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-radio-cards__card\"\n\t\t\t\t:class=\"{ 'cn-radio-cards__card--selected': isSelected(option) }\"\n\t\t\t\t:data-testid=\"`cn-radio-cards-option-${option.value}`\">\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-radio-cards__input\"\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"readOnly || busy\"\n\t\t\t\t\t@change=\"choose(option)\">\n\t\t\t\t<span class=\"cn-radio-cards__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span class=\"cn-radio-cards__text\">\n\t\t\t\t\t<span class=\"cn-radio-cards__title\">{{ option.label }}</span>\n\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-radio-cards__description\">{{ option.description }}</span>\n\t\t\t\t</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<NcNoteCard v-if=\"notice\" type=\"error\" data-testid=\"cn-radio-cards-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</fieldset>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\nlet uid = 0\n\n/**\n * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,\n * each with a title and a description, bound to an enum field of the CURRENT\n * object. Choosing a card saves at once with ONE PATCH of that field; a refused\n * PATCH puts the choice back and shows the server's words. Real radio inputs\n * underneath, so the choice is perceivable without colour and works from the\n * keyboard.\n *\n * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md\n */\nexport default {\n\tname: 'CnRadioCardsWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the enum field, a top-level name or a dot-path.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `options` (required) — `[{ value, label, description? }]`.\n\t\t * - `label` — a legend over the cards (visually shown).\n\t\t * - `columns` — 1, 2 or 3 (default 2).\n\t\t * - `readOnly`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\tuid += 1\n\t\treturn {\n\t\t\tgroupName: `cn-radio-cards-${uid}`,\n\t\t\tsaved: '',\n\t\t\tvalue: '',\n\t\t\tbusy: false,\n\t\t\tnotice: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\toptions() {\n\t\t\tconst list = Array.isArray(this.content.options) ? this.content.options : []\n\t\t\treturn list\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => (typeof option === 'object'\n\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }\n\t\t\t\t\t: { value: option, label: this.tr(String(option)), description: '' }))\n\t\t},\n\n\t\tlabel() {\n\t\t\treturn this.tr(this.content.label || '')\n\t\t},\n\n\t\tcolumns() {\n\t\t\treturn [1, 3].includes(this.content.columns) ? this.content.columns : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether the widget writes through the app's own endpoint (`content.save`). */\n\t\thasSave() {\n\t\t\treturn !!(this.content.save && this.content.save.url)\n\t\t},\n\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/** Read the choice off the object. */\n\t\tadopt() {\n\t\t\tconst stored = readPath(this.objectData || {}, this.content.field)\n\t\t\tthis.saved = stored === undefined || stored === null ? '' : String(stored)\n\t\t\tthis.value = this.saved\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.value === String(option.value)\n\t\t},\n\n\t\t/**\n\t\t * Choose a card and save that one field.\n\t\t *\n\t\t * @param {object} option The chosen option.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field\n\t\t */\n\t\tasync choose(option) {\n\t\t\tif ((!this.objectUrl && !this.hasSave) || !this.content.field) {\n\t\t\t\tthis.notice = this.tr('This choice cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.notice = ''\n\t\t\tthis.busy = true\n\t\t\tthis.value = String(option.value)\n\t\t\ttry {\n\t\t\t\tconst data = await writeFields(this.objectUrl, { [this.content.field]: option.value }, { save: this.content.save, object: this.objectData })\n\t\t\t\tconst written = readPath(data, this.content.field)\n\t\t\t\tthis.saved = String(written !== undefined && written !== null ? written : option.value)\n\t\t\t\tthis.value = this.saved\n\t\t\t\t/**\n\t\t\t\t * @event saved The choice was written.\n\t\t\t\t * @type {{field: string, value: unknown}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: option.value })\n\t\t\t} catch (e) {\n\t\t\t\tthis.value = this.saved\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.busy = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-radio-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-width: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n}\n\n.cn-radio-cards__legend {\n\tpadding: 0;\n\tmargin-bottom: 6px;\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__grid {\n\tdisplay: grid;\n\tgap: 10px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-radio-cards__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-radio-cards__grid--3 {\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.cn-radio-cards__card {\n\tposition: relative;\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 10px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-radio-cards__card--selected {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n\tbox-shadow: 0 0 0 1px var(--color-primary-element);\n}\n\n.cn-radio-cards__card:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */\n.cn-radio-cards__input {\n\tposition: absolute;\n\topacity: 0;\n\tinset: 0;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n.cn-radio-cards__dot {\n\tflex: 0 0 auto;\n\twidth: 14px;\n\theight: 14px;\n\tmargin-top: 3px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: 50%;\n\tbackground: var(--color-main-background);\n}\n\n.cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder: 4px solid var(--color-primary-element);\n}\n\n.cn-radio-cards__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-radio-cards__title {\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__description {\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n@media (max-width: 640px) {\n\t.cn-radio-cards__grid--2,\n\t.cn-radio-cards__grid--3 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_toDisplayString","_normalizeClass","_Fragment","_renderList","_createElementVNode"],"mappings":";;;;;AAYuB,EAAA,KAAK,EAAA,wBAAA;;;;AAmBrB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,sBAAA,GAAA;AACI,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAwB,GAAA;;;AAC3B,EAAA,KAAA,EAAA,6BAAA;;;;;;;AAtBN,IAAA,KAAA,EAAA,gBAAA;AAAA,IAAA,aAAA,EAAA,gBAAA;AAAA,IAAA,WAAA,EAAA,KAAA,CAAA,IAAA,GAAA,MAAA,GAAA,OAAA;;KACQ,QAAK,CAAA,KAAA;wCAEV,CAAA,QAAA,YAAA,EAAAA,eAAA,CADL,QAAK,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;MAEJ,KAAK,EAAAC,cAAA,CAAA,CAAE,sBAAqB,EAAA,CAAA,sBAAA,EAAiC,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;0CAoBzD,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAlBF,QAAA,CAAA,OAAA,EAAA,CAAA,MAAA,KAAA;+CAkBE,CAAA,OAAA,EAAA;AAjBN,UAAA,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;eACpB,EAACF,cAAA,CAAA,CAAA,sBAAM,EACK,EAAA,gCAAA,EAAA,QAAA,CAAA,UAAA,CAAA,MAAA,CAAA,EAAA,CAAA,CAAA;UAChB,aAAW,EAAA,CAAA,sBAAA,EAAiB,MAAA,CAAA,KAAA,CAAA,CAAA;;4BAQvB,CAAA,OAAA,EAAA;AANL,YAAA,KAAK,EAAC,uBAAqB;AAC3B,YAAA,IAAI,EAAC,OAAO;AACX,YAAA,IAAI,EAAE,KAAS,CAAA,SAAA;AACf,YAAA,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;YAC1B,OAAO,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;YAC1B,QAAQ,EAAE,QAAQ,CAAA,QAAA,IAAI,KAAI,CAAA,IAAA;YAC1B,QAAI,EAAA,MAAA,KAAA,QAAA,CAAA,MAAA,CAAA,MAAA,CAAA,CAAA;;oCACNG,kBAAM,CAAA,MAAA,EAAA;AAAA,YAAA,KAAA,EAAA,qBAAA;AAAA,YAAA,aAAA,EAAA,MAAA;;AACN,UAAAA,kBAAA,CAGO,QAHP,UAGO,EAAA;AAFN,YAAAA,kBAAA,CAA6D,MAA7D,EAAA,UAA6D,EAAtBJ,eAAA,CAAA,MAAM,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;AAC1C,YAAA,CAAA,MAAA,CAAA,WAAA;iDAAA,kBAAA,EAAAA,eAAA,CAAA,MAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnRadioCardsWidget.vue3.js","sources":["../../../../src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue?vue&type=template&id=8ac292d6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<fieldset class=\"cn-radio-cards\"\n\t\t:class=\"{ 'cn-radio-cards--list': isList }\"\n\t\tdata-testid=\"cn-radio-cards\"\n\t\t:aria-busy=\"busy ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-radio-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\t\t<div class=\"cn-radio-cards__grid\" :class=\"`cn-radio-cards__grid--${columns}`\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in options\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-radio-cards__card\"\n\t\t\t\t:class=\"{ 'cn-radio-cards__card--selected': isSelected(option) }\"\n\t\t\t\t:data-testid=\"`cn-radio-cards-option-${option.value}`\">\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-radio-cards__input\"\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"readOnly || busy\"\n\t\t\t\t\t@change=\"choose(option)\">\n\t\t\t\t<span class=\"cn-radio-cards__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span class=\"cn-radio-cards__text\">\n\t\t\t\t\t<span class=\"cn-radio-cards__title\">{{ option.label }}</span>\n\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-radio-cards__description\">{{ option.description }}</span>\n\t\t\t\t</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<NcNoteCard v-if=\"notice\" type=\"error\" data-testid=\"cn-radio-cards-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</fieldset>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\nlet uid = 0\n\n/**\n * CnRadioCardsWidget — the `radio-cards` widget: one choice out of a few cards,\n * each with a title and a description, bound to an enum field of the CURRENT\n * object. Choosing a card saves at once with ONE PATCH of that field; a refused\n * PATCH puts the choice back and shows the server's words. Real radio inputs\n * underneath, so the choice is perceivable without colour and works from the\n * keyboard.\n *\n * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md\n */\nexport default {\n\tname: 'CnRadioCardsWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the enum field, a top-level name or a dot-path.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `options` (required) — `[{ value, label, description? }]`.\n\t\t * - `label` — a legend over the cards (visually shown).\n\t\t * - `columns` — 1, 2 or 3 (default 2).\n\t\t * - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.\n\t\t * - `readOnly`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\tuid += 1\n\t\treturn {\n\t\t\tgroupName: `cn-radio-cards-${uid}`,\n\t\t\tsaved: '',\n\t\t\tvalue: '',\n\t\t\tbusy: false,\n\t\t\tnotice: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\toptions() {\n\t\t\tconst list = Array.isArray(this.content.options) ? this.content.options : []\n\t\t\treturn list\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => (typeof option === 'object'\n\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)), description: this.tr(option.description || '') }\n\t\t\t\t\t: { value: option, label: this.tr(String(option)), description: '' }))\n\t\t},\n\n\t\tlabel() {\n\t\t\treturn this.tr(this.content.label || '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the options draw as a compact list (`variant: \"list\"`).\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list\n\t\t */\n\t\tisList() {\n\t\t\treturn this.content.variant === 'list'\n\t\t},\n\n\t\tcolumns() {\n\t\t\tif (this.isList) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn [1, 3].includes(this.content.columns) ? this.content.columns : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether the widget writes through the app's own endpoint (`content.save`). */\n\t\thasSave() {\n\t\t\treturn !!(this.content.save && this.content.save.url)\n\t\t},\n\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/** Read the choice off the object. */\n\t\tadopt() {\n\t\t\tconst stored = readPath(this.objectData || {}, this.content.field)\n\t\t\tthis.saved = stored === undefined || stored === null ? '' : String(stored)\n\t\t\tthis.value = this.saved\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.value === String(option.value)\n\t\t},\n\n\t\t/**\n\t\t * Choose a card and save that one field.\n\t\t *\n\t\t * @param {object} option The chosen option.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field\n\t\t */\n\t\tasync choose(option) {\n\t\t\tif ((!this.objectUrl && !this.hasSave) || !this.content.field) {\n\t\t\t\tthis.notice = this.tr('This choice cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.notice = ''\n\t\t\tthis.busy = true\n\t\t\tthis.value = String(option.value)\n\t\t\ttry {\n\t\t\t\tconst data = await writeFields(this.objectUrl, { [this.content.field]: option.value }, { save: this.content.save, object: this.objectData })\n\t\t\t\tconst written = readPath(data, this.content.field)\n\t\t\t\tthis.saved = String(written !== undefined && written !== null ? written : option.value)\n\t\t\t\tthis.value = this.saved\n\t\t\t\t/**\n\t\t\t\t * @event saved The choice was written.\n\t\t\t\t * @type {{field: string, value: unknown}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: option.value })\n\t\t\t} catch (e) {\n\t\t\t\tthis.value = this.saved\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.busy = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-radio-cards {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-width: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n}\n\n.cn-radio-cards__legend {\n\tpadding: 0;\n\tmargin-bottom: 6px;\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__grid {\n\tdisplay: grid;\n\tgap: 10px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-radio-cards__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-radio-cards__grid--3 {\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.cn-radio-cards__card {\n\tposition: relative;\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 10px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-radio-cards__card--selected {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n\tbox-shadow: 0 0 0 1px var(--color-primary-element);\n}\n\n.cn-radio-cards__card:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n/* The real input stays in the DOM and in the accessibility tree; the dot is its picture. */\n.cn-radio-cards__input {\n\tposition: absolute;\n\topacity: 0;\n\tinset: 0;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n.cn-radio-cards__dot {\n\tflex: 0 0 auto;\n\twidth: 14px;\n\theight: 14px;\n\tmargin-top: 3px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: 50%;\n\tbackground: var(--color-main-background);\n}\n\n.cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder: 4px solid var(--color-primary-element);\n}\n\n.cn-radio-cards__text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-radio-cards__title {\n\tfont-weight: 600;\n}\n\n.cn-radio-cards__description {\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* `variant: \"list\"`: one radio button per line, the description under the\n label, no card around each (board DcBeheer, \"Soort organisatie\"). */\n.cn-radio-cards--list .cn-radio-cards__grid {\n\tgap: 8px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__card,\n.cn-radio-cards--list .cn-radio-cards__card--selected {\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n\tbox-shadow: none;\n\tfont-size: 15px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__dot {\n\twidth: 18px;\n\theight: 18px;\n\tmargin-top: 2px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__card--selected .cn-radio-cards__dot {\n\tborder-width: 5px;\n}\n\n.cn-radio-cards--list .cn-radio-cards__title {\n\tfont-weight: 400;\n}\n\n.cn-radio-cards--list .cn-radio-cards__description {\n\tfont-size: 13px;\n}\n\n@media (max-width: 640px) {\n\t.cn-radio-cards__grid--2,\n\t.cn-radio-cards__grid--3 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_toDisplayString","_Fragment","_renderList","_createElementVNode"],"mappings":";;;;;AAeuB,EAAA,KAAK,EAAA,wBAAA;;;;AAmBrB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,sBAAA,GAAA;AACI,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAwB,GAAA;;;AAC3B,EAAA,KAAA,EAAA,6BAAA;;;;;;uBAzBXA;SAAU,EAAAC,cAAA,CAAA,CAAA,gBAAA,EAAA,EAAA,sBAAA,EACH,QAAA,CAAA,MAAA,EAAA,CAAA,CAAA;AACR,IAAA,aAAW,EAAC,gBAAgB;AAC3B,IAAA,WAAK,EAAA,KAAA,CAAA,IAAA,GAAA,MAAA,GAAA,OAAA;;KACQ,QAAK,CAAA,KAAA;wCAEV,CAAA,QAAA,YAAA,EAAAC,eAAA,CADL,QAAK,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;;MAEJ,KAAK,EAAAD,cAAA,CAAA,CAAE,sBAAqB,EAAA,CAAA,sBAAA,EAAiC,QAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA;;0CAoBzD,CAAAE,QAAA,EAAA,IAAA,EAAAC,UAAA,CAlBF,QAAA,CAAA,OAAA,EAAA,CAAA,MAAA,KAAA;+CAkBE,CAAA,OAAA,EAAA;AAjBN,UAAA,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;eACpB,EAACH,cAAA,CAAA,CAAA,sBAAM,EACK,EAAA,gCAAA,EAAA,QAAA,CAAA,UAAA,CAAA,MAAA,CAAA,EAAA,CAAA,CAAA;UAChB,aAAW,EAAA,CAAA,sBAAA,EAAiB,MAAA,CAAA,KAAA,CAAA,CAAA;;4BAQvB,CAAA,OAAA,EAAA;AANL,YAAA,KAAK,EAAC,uBAAqB;AAC3B,YAAA,IAAI,EAAC,OAAO;AACX,YAAA,IAAI,EAAE,KAAS,CAAA,SAAA;AACf,YAAA,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;YAC1B,OAAO,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;YAC1B,QAAQ,EAAE,QAAQ,CAAA,QAAA,IAAI,KAAI,CAAA,IAAA;YAC1B,QAAI,EAAA,MAAA,KAAA,QAAA,CAAA,MAAA,CAAA,MAAA,CAAA,CAAA;;oCACNI,kBAAM,CAAA,MAAA,EAAA;AAAA,YAAA,KAAA,EAAA,qBAAA;AAAA,YAAA,aAAA,EAAA,MAAA;;AACN,UAAAA,kBAAA,CAGO,QAHP,UAGO,EAAA;AAFN,YAAAA,kBAAA,CAA6D,MAA7D,EAAA,UAA6D,EAAtBH,eAAA,CAAA,MAAM,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;AAC1C,YAAA,CAAA,MAAA,CAAA,WAAA;iDAAA,kBAAA,EAAAA,eAAA,CAAA,MAAA,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnReportsPage.vue2.js';
2
2
  import { render } from './CnReportsPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-7597ff10";
5
+ script.__scopeId = "data-v-7629715a";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,5 +1,7 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
2
  import { defineAsyncComponent } from 'vue';
3
+ import __sfc_main from '../CnIcon/CnIcon.vue.js';
4
+ import { useLook } from '../../composables/useLook.js';
3
5
  import { followLinkClick } from '../../utils/linkNavigation.js';
4
6
 
5
7
  var script = {
@@ -9,6 +11,7 @@ var script = {
9
11
  // Loaded only for a page that declares widgets: a cards-only reports
10
12
  // page does not pull the dashboard in.
11
13
  CnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue.js').then((m) => m.default)),
14
+ CnIcon: __sfc_main,
12
15
  },
13
16
 
14
17
  inject: {
@@ -130,6 +133,17 @@ var script = {
130
133
  default: null,
131
134
  },
132
135
 
136
+ /**
137
+ * The look to draw in: `board` or `nextcloud`. Absent: the look
138
+ * CnAppRoot provides (`cnLook`), else `nextcloud`.
139
+ *
140
+ * @type {string|null}
141
+ */
142
+ look: {
143
+ type: String,
144
+ default: null,
145
+ },
146
+
133
147
  /**
134
148
  * Translate function. Falls back to injected `cnTranslate`.
135
149
  *
@@ -141,6 +155,11 @@ var script = {
141
155
  },
142
156
  },
143
157
 
158
+ setup(props) {
159
+ const { isBoard } = useLook(props);
160
+ return { isBoard }
161
+ },
162
+
144
163
  data() {
145
164
  return {
146
165
  activeCategory: 'all',
@@ -287,6 +306,30 @@ var script = {
287
306
  return options
288
307
  },
289
308
 
309
+ /**
310
+ * The board look's sections: the cards without a category first, under
311
+ * no heading, then one section per declared category that holds a
312
+ * card, in the declared order, headed by its label.
313
+ *
314
+ * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look
315
+ * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.
316
+ */
317
+ boardSections() {
318
+ const declared = this.config.categories || {};
319
+ const sections = [];
320
+ const loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category));
321
+ if (loose.length > 0) {
322
+ sections.push({ key: '__none', label: '', cards: loose });
323
+ }
324
+ for (const key of Object.keys(declared)) {
325
+ const cards = this.resolvedCards.filter((card) => card.category === key);
326
+ if (cards.length > 0) {
327
+ sections.push({ key, label: this.tr(declared[key]), cards });
328
+ }
329
+ }
330
+ return sections
331
+ },
332
+
290
333
  /**
291
334
  * The cards the active filter admits.
292
335
  *