@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,230 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LookupPickerDialog - Modal search dialog for entity lookup
4
+ *
5
+ * Opens a DataTable with search to select item(s) from a large dataset.
6
+ * Supports single selection (click row) and multiple selection (checkboxes).
7
+ *
8
+ * Used internally by LookupField in 'picker' mode.
9
+ */
10
+ import { ref, computed, watch } from 'vue'
11
+ import Dialog from 'primevue/dialog'
12
+ import DataTable from 'primevue/datatable'
13
+ import Column from 'primevue/column'
14
+ import InputText from 'primevue/inputtext'
15
+ import Button from 'primevue/button'
16
+ import { useI18n } from '../../i18n/useI18n'
17
+
18
+ const { t } = useI18n()
19
+
20
+ export interface LookupColumn {
21
+ field: string
22
+ header?: string
23
+ sortable?: boolean
24
+ style?: string | Record<string, string>
25
+ }
26
+
27
+ interface Props {
28
+ visible: boolean
29
+ title?: string
30
+ items: Record<string, unknown>[]
31
+ columns: (string | LookupColumn)[]
32
+ loading?: boolean
33
+ valueField?: string
34
+ /** Current value(s) — single value or array for multiple mode */
35
+ currentValue?: unknown
36
+ width?: string
37
+ /** Enable checkbox multi-selection */
38
+ multiple?: boolean
39
+ }
40
+
41
+ const props = withDefaults(defineProps<Props>(), {
42
+ title: 'Select item',
43
+ loading: false,
44
+ valueField: 'id',
45
+ currentValue: undefined,
46
+ width: '700px',
47
+ multiple: false,
48
+ })
49
+
50
+ const emit = defineEmits<{
51
+ 'update:visible': [value: boolean]
52
+ /** Single selection (multiple=false) */
53
+ 'select': [item: Record<string, unknown>]
54
+ /** Multi selection (multiple=true) */
55
+ 'select-multiple': [items: Record<string, unknown>[]]
56
+ }>()
57
+
58
+ const searchQuery = ref('')
59
+ // Single mode
60
+ const selectedRow = ref<Record<string, unknown> | null>(null)
61
+ // Multi mode
62
+ const selectedRows = ref<Record<string, unknown>[]>([])
63
+
64
+ // Reset state + pre-select current values when dialog opens
65
+ watch(() => props.visible, (open) => {
66
+ if (open) {
67
+ searchQuery.value = ''
68
+ selectedRow.value = null
69
+ if (props.multiple && props.currentValue != null) {
70
+ // Pre-select rows matching currentValue array
71
+ const vals = Array.isArray(props.currentValue) ? props.currentValue : [props.currentValue]
72
+ const valStrings = new Set(vals.map(String))
73
+ selectedRows.value = props.items.filter((item) =>
74
+ valStrings.has(String(item[props.valueField])),
75
+ )
76
+ } else {
77
+ selectedRows.value = []
78
+ }
79
+ }
80
+ })
81
+
82
+ // Resolve columns config
83
+ const resolvedColumns = computed((): LookupColumn[] => {
84
+ return props.columns.map((col) => {
85
+ if (typeof col === 'string') {
86
+ return { field: col, header: col.charAt(0).toUpperCase() + col.slice(1), sortable: true }
87
+ }
88
+ return { header: col.field.charAt(0).toUpperCase() + col.field.slice(1), sortable: true, ...col }
89
+ })
90
+ })
91
+
92
+ // Filter items by search query across all visible columns
93
+ const filteredItems = computed(() => {
94
+ if (!searchQuery.value) return props.items
95
+ const q = searchQuery.value.toLowerCase()
96
+ return props.items.filter((item) =>
97
+ resolvedColumns.value.some((col) => {
98
+ const val = item[col.field]
99
+ return val != null && String(val).toLowerCase().includes(q)
100
+ }),
101
+ )
102
+ })
103
+
104
+ // Highlight the row(s) matching currentValue (single mode only)
105
+ const rowClass = (data: Record<string, unknown>): string | undefined => {
106
+ if (props.multiple) return undefined
107
+ const val = data[props.valueField]
108
+ if (val != null && props.currentValue != null && String(val) === String(props.currentValue)) {
109
+ return 'lookup-current-row'
110
+ }
111
+ return undefined
112
+ }
113
+
114
+ function onRowSelect(event: { data: Record<string, unknown> }): void {
115
+ if (!props.multiple) {
116
+ selectedRow.value = event.data
117
+ }
118
+ }
119
+
120
+ function onRowDblClick(event: { data: Record<string, unknown> }): void {
121
+ if (!props.multiple) {
122
+ emit('select', event.data)
123
+ emit('update:visible', false)
124
+ }
125
+ }
126
+
127
+ function onConfirm(): void {
128
+ if (props.multiple) {
129
+ emit('select-multiple', selectedRows.value)
130
+ } else if (selectedRow.value) {
131
+ emit('select', selectedRow.value)
132
+ }
133
+ emit('update:visible', false)
134
+ }
135
+
136
+ function onCancel(): void {
137
+ emit('update:visible', false)
138
+ }
139
+
140
+ const confirmDisabled = computed(() => {
141
+ if (props.multiple) return false // allow confirming empty selection (clear all)
142
+ return !selectedRow.value
143
+ })
144
+
145
+ const confirmLabel = computed(() => {
146
+ if (props.multiple && selectedRows.value.length > 0) {
147
+ return `Select (${selectedRows.value.length})`
148
+ }
149
+ return 'Select'
150
+ })
151
+ </script>
152
+
153
+ <template>
154
+ <Dialog
155
+ :visible="visible"
156
+ :header="title"
157
+ :modal="true"
158
+ :style="{ width }"
159
+ :closable="true"
160
+ :dismissableMask="true"
161
+ @update:visible="$emit('update:visible', $event)"
162
+ >
163
+ <!-- Search bar -->
164
+ <div class="flex gap-2 mb-3">
165
+ <span class="p-input-icon-left flex-1">
166
+ <i class="pi pi-search" />
167
+ <InputText
168
+ v-model="searchQuery"
169
+ :placeholder="t('core.placeholders.search')"
170
+ class="w-full"
171
+ autofocus
172
+ />
173
+ </span>
174
+ </div>
175
+
176
+ <!-- Results table -->
177
+ <DataTable
178
+ :value="filteredItems"
179
+ :loading="loading"
180
+ v-model:selection="selectedRows"
181
+ :selectionMode="multiple ? undefined : 'single'"
182
+ @row-select="onRowSelect"
183
+ @row-dblclick="onRowDblClick"
184
+ :rowClass="rowClass"
185
+ :paginator="filteredItems.length > 10"
186
+ :rows="10"
187
+ :dataKey="valueField"
188
+ scrollable
189
+ scrollHeight="400px"
190
+ stripedRows
191
+ class="lookup-picker-table"
192
+ >
193
+ <!-- Checkbox column for multi mode -->
194
+ <Column v-if="multiple" selectionMode="multiple" headerStyle="width: 3rem" />
195
+ <Column
196
+ v-for="col in resolvedColumns"
197
+ :key="col.field"
198
+ :field="col.field"
199
+ :header="col.header"
200
+ :sortable="col.sortable"
201
+ :style="col.style"
202
+ />
203
+ <template #empty>
204
+ <div class="text-center text-color-secondary py-4">
205
+ {{ searchQuery ? t('core.messages.noMatching') : t('core.messages.empty') }}
206
+ </div>
207
+ </template>
208
+ </DataTable>
209
+
210
+ <template #footer>
211
+ <Button
212
+ :label="t('core.actions.cancel')"
213
+ severity="secondary"
214
+ @click="onCancel"
215
+ />
216
+ <Button
217
+ :label="confirmLabel"
218
+ icon="pi pi-check"
219
+ :disabled="confirmDisabled"
220
+ @click="onConfirm"
221
+ />
222
+ </template>
223
+ </Dialog>
224
+ </template>
225
+
226
+ <style scoped>
227
+ .lookup-picker-table :deep(.lookup-current-row) {
228
+ background-color: var(--p-highlight-background) !important;
229
+ }
230
+ </style>
@@ -0,0 +1,470 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * JsonEditorFoldable - JSON editor with collapsible sections
4
+ *
5
+ * Displays each top-level key as a foldable section.
6
+ * Long content (like "content" field) can be collapsed to reduce visual noise.
7
+ *
8
+ * Usage:
9
+ * <JsonEditorFoldable v-model="jsonData" :defaultExpanded="['content']" />
10
+ */
11
+
12
+ import { ref, computed, watch } from 'vue'
13
+ import Button from 'primevue/button'
14
+ import Textarea from 'primevue/textarea'
15
+ import InputText from 'primevue/inputtext'
16
+ import Message from 'primevue/message'
17
+ import { getJsonValueType, getJsonPreview } from '../../composables/useJsonSyntax'
18
+
19
+ interface Props {
20
+ modelValue?: Record<string, unknown> | null
21
+ defaultExpanded?: string[]
22
+ height?: string
23
+ }
24
+
25
+ const props = withDefaults(defineProps<Props>(), {
26
+ modelValue: () => ({}),
27
+ defaultExpanded: () => ['content'],
28
+ height: '400px'
29
+ })
30
+
31
+ const emit = defineEmits<{
32
+ 'update:modelValue': [value: Record<string, unknown>]
33
+ }>()
34
+
35
+ // Internal state: track which sections are expanded
36
+ const expandedSections = ref<Set<string>>(new Set(props.defaultExpanded))
37
+
38
+ // Internal copy of data for editing
39
+ const localData = ref<Record<string, unknown>>({})
40
+
41
+ // Parse error for raw JSON mode
42
+ const parseError = ref<string | null>(null)
43
+ const rawJsonMode = ref<boolean>(false)
44
+ const rawJsonText = ref<string>('')
45
+
46
+ // Sync from modelValue
47
+ watch(() => props.modelValue, (newVal: Record<string, unknown> | null | undefined) => {
48
+ if (newVal && typeof newVal === 'object') {
49
+ localData.value = JSON.parse(JSON.stringify(newVal))
50
+ rawJsonText.value = JSON.stringify(newVal, null, 2)
51
+ } else {
52
+ localData.value = {}
53
+ rawJsonText.value = '{}'
54
+ }
55
+ }, { immediate: true, deep: true })
56
+
57
+ // Computed: sorted keys with "content" first if present
58
+ const sortedKeys = computed<string[]>(() => {
59
+ const keys = Object.keys(localData.value || {})
60
+ // Put 'content' first, then sort rest alphabetically
61
+ return keys.sort((a, b) => {
62
+ if (a === 'content') return -1
63
+ if (b === 'content') return 1
64
+ return a.localeCompare(b)
65
+ })
66
+ })
67
+
68
+ function isExpanded(key: string): boolean {
69
+ return expandedSections.value.has(key)
70
+ }
71
+
72
+ function toggleSection(key: string): void {
73
+ if (expandedSections.value.has(key)) {
74
+ expandedSections.value.delete(key)
75
+ } else {
76
+ expandedSections.value.add(key)
77
+ }
78
+ // Trigger reactivity
79
+ expandedSections.value = new Set(expandedSections.value)
80
+ }
81
+
82
+ function expandAll(): void {
83
+ expandedSections.value = new Set(sortedKeys.value)
84
+ }
85
+
86
+ function collapseAll(): void {
87
+ expandedSections.value = new Set()
88
+ }
89
+
90
+ // Update a specific field
91
+ function updateField(key: string, newValue: unknown): void {
92
+ const type = getJsonValueType(localData.value[key])
93
+
94
+ if (type === 'string') {
95
+ localData.value[key] = newValue
96
+ } else if (type === 'number') {
97
+ localData.value[key] = parseFloat(String(newValue)) || 0
98
+ } else if (type === 'boolean') {
99
+ localData.value[key] = newValue === 'true' || newValue === true
100
+ } else if (type === 'array' || type === 'object') {
101
+ // Parse as JSON for complex types
102
+ try {
103
+ localData.value[key] = JSON.parse(String(newValue))
104
+ } catch {
105
+ // Invalid JSON, don't update
106
+ return
107
+ }
108
+ }
109
+
110
+ emitUpdate()
111
+ }
112
+
113
+ function emitUpdate(): void {
114
+ emit('update:modelValue', JSON.parse(JSON.stringify(localData.value)))
115
+ }
116
+
117
+ // Raw JSON mode handling
118
+ function toggleRawMode(): void {
119
+ if (rawJsonMode.value) {
120
+ // Switching from raw to structured - parse the raw JSON
121
+ try {
122
+ localData.value = JSON.parse(rawJsonText.value)
123
+ parseError.value = null
124
+ emitUpdate()
125
+ } catch (e) {
126
+ parseError.value = (e as Error).message
127
+ return // Don't switch if invalid
128
+ }
129
+ } else {
130
+ // Switching to raw mode - serialize current data
131
+ rawJsonText.value = JSON.stringify(localData.value, null, 2)
132
+ }
133
+ rawJsonMode.value = !rawJsonMode.value
134
+ }
135
+
136
+ function onRawJsonInput(event: Event): void {
137
+ rawJsonText.value = (event.target as HTMLTextAreaElement).value
138
+ try {
139
+ JSON.parse(rawJsonText.value)
140
+ parseError.value = null
141
+ } catch (e) {
142
+ parseError.value = (e as Error).message
143
+ }
144
+ }
145
+
146
+ function saveRawJson(): void {
147
+ try {
148
+ localData.value = JSON.parse(rawJsonText.value)
149
+ parseError.value = null
150
+ emitUpdate()
151
+ } catch (e) {
152
+ parseError.value = (e as Error).message
153
+ }
154
+ }
155
+
156
+ // Get appropriate editor for value type
157
+ function isMultiline(value: unknown): boolean {
158
+ if (typeof value !== 'string') return false
159
+ return value.length > 100 || value.includes('\n')
160
+ }
161
+ </script>
162
+
163
+ <template>
164
+ <div class="json-editor-foldable">
165
+ <!-- Toolbar -->
166
+ <div class="editor-toolbar">
167
+ <div class="toolbar-left">
168
+ <Button
169
+ :icon="rawJsonMode ? 'pi pi-list' : 'pi pi-code'"
170
+ :label="rawJsonMode ? 'Structured' : 'Raw JSON'"
171
+ size="small"
172
+ severity="secondary"
173
+ text
174
+ @click="toggleRawMode"
175
+ />
176
+ </div>
177
+ <div class="toolbar-right" v-if="!rawJsonMode">
178
+ <Button
179
+ icon="pi pi-plus"
180
+ label="Expand All"
181
+ size="small"
182
+ severity="secondary"
183
+ text
184
+ @click="expandAll"
185
+ />
186
+ <Button
187
+ icon="pi pi-minus"
188
+ label="Collapse All"
189
+ size="small"
190
+ severity="secondary"
191
+ text
192
+ @click="collapseAll"
193
+ />
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Structured Mode -->
198
+ <div v-if="!rawJsonMode" class="sections-container" :style="{ maxHeight: height }">
199
+ <div
200
+ v-for="key in sortedKeys"
201
+ :key="key"
202
+ class="section"
203
+ :class="{ expanded: isExpanded(key) }"
204
+ >
205
+ <div class="section-header" @click="toggleSection(key)">
206
+ <i
207
+ class="pi"
208
+ :class="isExpanded(key) ? 'pi-chevron-down' : 'pi-chevron-right'"
209
+ ></i>
210
+ <span class="section-key">{{ key }}</span>
211
+ <span class="section-type">{{ getJsonValueType(localData[key]) }}</span>
212
+ <span v-if="!isExpanded(key)" class="section-preview">{{ getJsonPreview(localData[key]) }}</span>
213
+ </div>
214
+
215
+ <div v-if="isExpanded(key)" class="section-content">
216
+ <!-- String (multiline) -->
217
+ <Textarea
218
+ v-if="getJsonValueType(localData[key]) === 'string' && isMultiline(localData[key])"
219
+ :modelValue="String(localData[key])"
220
+ @update:modelValue="updateField(key, $event)"
221
+ rows="8"
222
+ class="w-full field-textarea"
223
+ autoResize
224
+ />
225
+
226
+ <!-- String (single line) -->
227
+ <InputText
228
+ v-else-if="getJsonValueType(localData[key]) === 'string'"
229
+ :modelValue="String(localData[key])"
230
+ @update:modelValue="updateField(key, $event)"
231
+ class="w-full"
232
+ />
233
+
234
+ <!-- Number -->
235
+ <InputText
236
+ v-else-if="getJsonValueType(localData[key]) === 'number'"
237
+ type="number"
238
+ :modelValue="String(localData[key])"
239
+ @update:modelValue="updateField(key, $event)"
240
+ class="w-full"
241
+ />
242
+
243
+ <!-- Boolean -->
244
+ <select
245
+ v-else-if="getJsonValueType(localData[key]) === 'boolean'"
246
+ :value="String(localData[key])"
247
+ @change="updateField(key, ($event.target as HTMLSelectElement)?.value)"
248
+ class="field-select"
249
+ >
250
+ <option value="true">true</option>
251
+ <option value="false">false</option>
252
+ </select>
253
+
254
+ <!-- Array or Object - show as JSON -->
255
+ <Textarea
256
+ v-else-if="getJsonValueType(localData[key]) === 'array' || getJsonValueType(localData[key]) === 'object'"
257
+ :modelValue="JSON.stringify(localData[key], null, 2)"
258
+ @update:modelValue="updateField(key, $event)"
259
+ rows="6"
260
+ class="w-full field-json"
261
+ autoResize
262
+ />
263
+
264
+ <!-- Null -->
265
+ <span v-else-if="getJsonValueType(localData[key]) === 'null'" class="null-value">null</span>
266
+ </div>
267
+ </div>
268
+
269
+ <div v-if="sortedKeys.length === 0" class="empty-state">
270
+ <i class="pi pi-inbox"></i>
271
+ <span>No data</span>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Raw JSON Mode -->
276
+ <div v-else class="raw-json-container" :style="{ height }">
277
+ <textarea
278
+ class="raw-json-textarea"
279
+ :class="{ 'has-error': parseError }"
280
+ :value="rawJsonText"
281
+ @input="onRawJsonInput"
282
+ spellcheck="false"
283
+ ></textarea>
284
+ <div v-if="parseError" class="raw-json-actions">
285
+ <Message severity="error" :closable="false" class="parse-error">
286
+ {{ parseError }}
287
+ </Message>
288
+ </div>
289
+ <div v-else class="raw-json-actions">
290
+ <Button
291
+ label="Apply Changes"
292
+ icon="pi pi-check"
293
+ size="small"
294
+ @click="saveRawJson"
295
+ />
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </template>
300
+
301
+ <style scoped>
302
+ /*
303
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
304
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
305
+ */
306
+ .json-editor-foldable {
307
+ display: flex;
308
+ flex-direction: column;
309
+ border: 1px solid var(--p-surface-300);
310
+ border-radius: 0.375rem;
311
+ background: var(--p-surface-0);
312
+ }
313
+
314
+ .editor-toolbar {
315
+ display: flex;
316
+ justify-content: space-between;
317
+ align-items: center;
318
+ padding: 0.5rem;
319
+ border-bottom: 1px solid var(--p-surface-200);
320
+ background: var(--p-surface-50);
321
+ }
322
+
323
+ .toolbar-left,
324
+ .toolbar-right {
325
+ display: flex;
326
+ gap: 0.25rem;
327
+ }
328
+
329
+ .sections-container {
330
+ overflow-y: auto;
331
+ padding: 0.5rem;
332
+ }
333
+
334
+ .section {
335
+ border: 1px solid var(--p-surface-200);
336
+ border-radius: 0.375rem;
337
+ margin-bottom: 0.5rem;
338
+ background: var(--p-surface-0);
339
+ }
340
+
341
+ .section:last-child {
342
+ margin-bottom: 0;
343
+ }
344
+
345
+ .section-header {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.5rem;
349
+ padding: 0.75rem;
350
+ cursor: pointer;
351
+ user-select: none;
352
+ background: var(--p-surface-50);
353
+ border-radius: 0.375rem;
354
+ }
355
+
356
+ .section.expanded .section-header {
357
+ border-bottom: 1px solid var(--p-surface-200);
358
+ border-radius: 0.375rem 0.375rem 0 0;
359
+ }
360
+
361
+ .section-header:hover {
362
+ background: var(--p-surface-100);
363
+ }
364
+
365
+ .section-header .pi {
366
+ font-size: 0.75rem;
367
+ color: var(--p-surface-500);
368
+ }
369
+
370
+ .section-key {
371
+ font-weight: 600;
372
+ color: var(--p-primary-700);
373
+ }
374
+
375
+ .section-type {
376
+ font-size: 0.75rem;
377
+ color: var(--p-surface-400);
378
+ padding: 0.125rem 0.375rem;
379
+ background: var(--p-surface-100);
380
+ border-radius: 0.25rem;
381
+ }
382
+
383
+ .section-preview {
384
+ flex: 1;
385
+ font-size: 0.8125rem;
386
+ color: var(--p-surface-500);
387
+ overflow: hidden;
388
+ text-overflow: ellipsis;
389
+ white-space: nowrap;
390
+ text-align: right;
391
+ }
392
+
393
+ .section-content {
394
+ padding: 0.75rem;
395
+ }
396
+
397
+ .field-textarea,
398
+ .field-json {
399
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
400
+ font-size: 0.8125rem;
401
+ line-height: 1.5;
402
+ }
403
+
404
+ .field-json {
405
+ background: var(--p-surface-50);
406
+ }
407
+
408
+ .field-select {
409
+ padding: 0.5rem;
410
+ border: 1px solid var(--p-surface-300);
411
+ border-radius: 0.375rem;
412
+ background: var(--p-surface-0);
413
+ font-size: 0.875rem;
414
+ }
415
+
416
+ .null-value {
417
+ color: var(--p-surface-400);
418
+ font-style: italic;
419
+ }
420
+
421
+ .empty-state {
422
+ display: flex;
423
+ flex-direction: column;
424
+ align-items: center;
425
+ justify-content: center;
426
+ padding: 2rem;
427
+ color: var(--p-surface-400);
428
+ gap: 0.5rem;
429
+ }
430
+
431
+ .empty-state .pi {
432
+ font-size: 2rem;
433
+ }
434
+
435
+ /* Raw JSON mode */
436
+ .raw-json-container {
437
+ display: flex;
438
+ flex-direction: column;
439
+ }
440
+
441
+ .raw-json-textarea {
442
+ flex: 1;
443
+ padding: 1rem;
444
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
445
+ font-size: 0.8125rem;
446
+ line-height: 1.5;
447
+ border: none;
448
+ outline: none;
449
+ resize: none;
450
+ background: var(--p-surface-50);
451
+ }
452
+
453
+ .raw-json-textarea.has-error {
454
+ background: var(--p-red-50);
455
+ }
456
+
457
+ .raw-json-actions {
458
+ padding: 0.5rem;
459
+ border-top: 1px solid var(--p-surface-200);
460
+ background: var(--p-surface-50);
461
+ }
462
+
463
+ .parse-error {
464
+ margin: 0;
465
+ }
466
+
467
+ .w-full {
468
+ width: 100%;
469
+ }
470
+ </style>