@ngbplatform/ui 1.3.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 (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,66 @@
1
+ import type { RouteLocationNormalizedLoaded, RouteLocationRaw, RouteRecordRaw } from 'vue-router'
2
+ import { buildGeneralJournalEntriesListPath, buildGeneralJournalEntriesPath } from '../accounting/navigation'
3
+ import { buildReportPageUrl } from '../reporting/navigation'
4
+
5
+ type AliasRouteLike = Pick<RouteLocationNormalizedLoaded, 'params' | 'query' | 'hash'>
6
+
7
+ function preserveAliasContext(to: AliasRouteLike, path: string): RouteLocationRaw {
8
+ return {
9
+ path,
10
+ query: to.query,
11
+ hash: to.hash,
12
+ }
13
+ }
14
+
15
+ export const ngbRouteAliasRedirectRoutes: RouteRecordRaw[] = [
16
+ {
17
+ path: '/documents/general_journal_entry',
18
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesListPath()),
19
+ },
20
+ {
21
+ path: '/documents/general_journal_entry/new',
22
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath()),
23
+ },
24
+ {
25
+ path: '/documents/general_journal_entry/:id',
26
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath(String(to.params.id ?? ''))),
27
+ },
28
+ {
29
+ path: '/documents/accounting.general_journal_entry',
30
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesListPath()),
31
+ },
32
+ {
33
+ path: '/documents/accounting.general_journal_entry/new',
34
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath()),
35
+ },
36
+ {
37
+ path: '/documents/accounting.general_journal_entry/:id',
38
+ redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath(String(to.params.id ?? ''))),
39
+ },
40
+ {
41
+ path: '/admin/accounting/posting-log',
42
+ redirect: to => preserveAliasContext(to, buildReportPageUrl('accounting.posting_log')),
43
+ },
44
+ {
45
+ path: '/admin/accounting/consistency',
46
+ redirect: to => preserveAliasContext(to, buildReportPageUrl('accounting.consistency')),
47
+ },
48
+ ]
49
+
50
+ export function normalizeNgbRouteAliasPath(path: string | null | undefined): string {
51
+ const value = String(path ?? '').trim()
52
+ if (!value) return ''
53
+
54
+ if (value.startsWith('/documents/accounting.general_journal_entry')) {
55
+ return value.replace('/documents/accounting.general_journal_entry', '/accounting/general-journal-entries')
56
+ }
57
+
58
+ if (value.startsWith('/documents/general_journal_entry')) {
59
+ return value.replace('/documents/general_journal_entry', '/accounting/general-journal-entries')
60
+ }
61
+
62
+ if (value === '/admin/accounting/posting-log') return buildReportPageUrl('accounting.posting_log')
63
+ if (value === '/admin/accounting/consistency') return buildReportPageUrl('accounting.consistency')
64
+
65
+ return value
66
+ }
@@ -0,0 +1,15 @@
1
+ export function normalizeRouteParam(value: unknown): string | null {
2
+ const raw = Array.isArray(value) ? value[0] : value
3
+ const normalized = String(raw ?? '').trim()
4
+ return normalized || null
5
+ }
6
+
7
+ export function normalizeRequiredRouteParam(value: unknown): string {
8
+ return normalizeRouteParam(value) ?? ''
9
+ }
10
+
11
+ export function normalizeEntityEditorIdRouteParam(value: unknown): string | undefined {
12
+ const normalized = normalizeRouteParam(value)
13
+ if (!normalized) return undefined
14
+ return normalized.toLowerCase() === 'new' ? undefined : normalized
15
+ }
@@ -0,0 +1,39 @@
1
+ import type { RouteLocationRaw, Router } from 'vue-router'
2
+ import type { ToastApi } from '../primitives/toast'
3
+ import { toErrorMessage } from '../utils/errorMessage'
4
+
5
+ type ToastsLike = Pick<ToastApi, 'push'>
6
+
7
+ export function buildAbsoluteAppUrl(router: Router, to: RouteLocationRaw): string {
8
+ const href = router.resolve(to).href
9
+ if (typeof window !== 'undefined' && window.location?.origin) {
10
+ return new URL(href, window.location.origin).toString()
11
+ }
12
+ return href
13
+ }
14
+
15
+ export async function copyAppLink(
16
+ router: Router,
17
+ toasts: ToastsLike,
18
+ to: RouteLocationRaw,
19
+ opts?: { title?: string; message?: string },
20
+ ): Promise<boolean> {
21
+ try {
22
+ const url = buildAbsoluteAppUrl(router, to)
23
+ if (!navigator?.clipboard?.writeText) throw new Error('Clipboard is not available.')
24
+ await navigator.clipboard.writeText(url)
25
+ toasts.push({
26
+ title: opts?.title ?? 'Link copied',
27
+ message: opts?.message ?? 'Shareable link copied to clipboard.',
28
+ tone: 'neutral',
29
+ })
30
+ return true
31
+ } catch (cause) {
32
+ toasts.push({
33
+ title: 'Could not copy link',
34
+ message: toErrorMessage(cause, 'Clipboard is not available.'),
35
+ tone: 'danger',
36
+ })
37
+ return false
38
+ }
39
+ }
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <section class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
3
+ <h2 class="text-sm font-semibold text-ngb-text">Security audit</h2>
4
+ <div class="mt-3 space-y-2">
5
+ <div
6
+ v-for="event in events"
7
+ :key="event.id"
8
+ class="grid grid-cols-[minmax(0,1fr),auto] gap-3 border-t border-ngb-border pt-2 first:border-t-0 first:pt-0"
9
+ >
10
+ <div class="min-w-0">
11
+ <div class="truncate text-sm text-ngb-text">{{ event.title }}</div>
12
+ <div v-if="event.actor" class="mt-0.5 truncate text-xs text-ngb-muted">{{ event.actor }}</div>
13
+ </div>
14
+ <time class="text-xs text-ngb-muted">{{ event.occurredAt }}</time>
15
+ </div>
16
+ <div v-if="events.length === 0" class="text-sm text-ngb-muted">No security audit events in this view.</div>
17
+ </div>
18
+ </section>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ withDefaults(defineProps<{
23
+ events?: Array<{
24
+ id: string
25
+ title: string
26
+ actor?: string | null
27
+ occurredAt: string
28
+ }>
29
+ }>(), {
30
+ events: () => [],
31
+ })
32
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div class="flex h-full min-h-[320px] items-center justify-center p-6">
3
+ <div class="w-full max-w-[520px] rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-center shadow-card">
4
+ <div class="mx-auto flex h-11 w-11 items-center justify-center rounded-full border border-ngb-border bg-ngb-bg text-ngb-muted">
5
+ <NgbIcon name="shield" :size="22" />
6
+ </div>
7
+ <h1 class="mt-4 text-lg font-semibold text-ngb-text">{{ title }}</h1>
8
+ <p class="mt-2 text-sm leading-6 text-ngb-muted">{{ message }}</p>
9
+ <div v-if="$slots.actions" class="mt-5 flex justify-center gap-2">
10
+ <slot name="actions" />
11
+ </div>
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import NgbIcon from '../primitives/NgbIcon.vue'
18
+
19
+ withDefaults(defineProps<{
20
+ title?: string
21
+ message?: string
22
+ }>(), {
23
+ title: 'Access denied',
24
+ message: 'Your current access profile does not allow this operation.',
25
+ })
26
+ </script>
27
+
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <section class="flex min-w-0 flex-col rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card" data-testid="effective-access-panel">
3
+ <div class="flex items-center justify-between gap-3 border-b border-ngb-border px-4 py-3">
4
+ <div class="min-w-0">
5
+ <h2 class="truncate text-sm font-semibold text-ngb-text">Effective access</h2>
6
+ <div class="mt-0.5 text-xs text-ngb-muted">Version {{ access?.accessVersion ?? '-' }}</div>
7
+ </div>
8
+ <NgbButton v-if="showRefresh" size="sm" variant="ghost" :disabled="loading" @click="$emit('refresh')">
9
+ <NgbIcon name="refresh" :size="15" />
10
+ Refresh
11
+ </NgbButton>
12
+ </div>
13
+
14
+ <div v-if="loading" class="p-4 text-sm text-ngb-muted">Loading...</div>
15
+ <div v-else-if="error" class="p-4 text-sm text-ngb-danger">{{ error }}</div>
16
+ <div v-else-if="!access || access.groups.length === 0" class="p-4 text-sm text-ngb-muted">No effective permissions.</div>
17
+ <div v-else class="min-h-0 flex-1 overflow-auto">
18
+ <section v-for="group in access.groups" :key="group.group" class="border-b border-ngb-border last:border-b-0">
19
+ <div class="bg-[var(--ngb-grid-header)] px-4 py-2 text-xs font-semibold uppercase tracking-wide text-ngb-muted">
20
+ {{ group.group }}
21
+ </div>
22
+ <div class="divide-y divide-ngb-border">
23
+ <div
24
+ v-for="resource in group.resources"
25
+ :key="`${resource.resourceKind}:${resource.resourceCode}`"
26
+ class="grid grid-cols-[minmax(0,1fr),minmax(160px,auto)] gap-3 px-4 py-3"
27
+ >
28
+ <div class="min-w-0">
29
+ <div class="truncate text-sm font-medium text-ngb-text">{{ resource.displayName }}</div>
30
+ <div class="mt-0.5 truncate font-mono text-[11px] text-ngb-muted">{{ resource.resourceKind }}.{{ resource.resourceCode }}</div>
31
+ </div>
32
+ <div class="flex flex-wrap justify-end gap-1.5">
33
+ <NgbBadge v-for="action in resource.actions" :key="action" tone="neutral">{{ action }}</NgbBadge>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </section>
38
+ </div>
39
+ </section>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import NgbBadge from '../primitives/NgbBadge.vue'
44
+ import NgbButton from '../primitives/NgbButton.vue'
45
+ import NgbIcon from '../primitives/NgbIcon.vue'
46
+ import type { EffectiveAccessDto } from './types'
47
+
48
+ withDefaults(defineProps<{
49
+ access: EffectiveAccessDto | null
50
+ loading?: boolean
51
+ error?: string | null
52
+ showRefresh?: boolean
53
+ }>(), {
54
+ showRefresh: true,
55
+ })
56
+
57
+ defineEmits<{
58
+ (e: 'refresh'): void
59
+ }>()
60
+ </script>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <div class="min-w-0 space-y-4" data-testid="permission-matrix">
3
+ <div class="flex flex-wrap items-center justify-between gap-3">
4
+ <NgbInput
5
+ v-model="query"
6
+ class="max-w-[360px]"
7
+ placeholder="Filter permissions"
8
+ :disabled="disabled"
9
+ />
10
+ <div class="text-xs text-ngb-muted tabular-nums">{{ selectedCount }} / {{ definitions.length }}</div>
11
+ </div>
12
+
13
+ <div v-if="groups.length === 0" class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-sm text-ngb-muted">
14
+ No permissions match the current filter.
15
+ </div>
16
+
17
+ <section
18
+ v-for="group in groups"
19
+ :key="group.group"
20
+ class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card"
21
+ >
22
+ <div class="flex flex-wrap items-center justify-between gap-3 border-b border-ngb-border px-4 py-3">
23
+ <div class="min-w-0">
24
+ <h2 class="truncate text-sm font-semibold text-ngb-text">{{ group.group }}</h2>
25
+ <div class="mt-0.5 text-xs text-ngb-muted tabular-nums">{{ selectedInGroup(group.permissions) }} / {{ group.permissions.length }}</div>
26
+ </div>
27
+ <NgbButton
28
+ size="sm"
29
+ variant="ghost"
30
+ :disabled="disabled"
31
+ @click="toggleGroup(group.permissions)"
32
+ >
33
+ <NgbIcon :name="isGroupFullySelected(group.permissions) ? 'minus' : 'check'" :size="15" />
34
+ {{ isGroupFullySelected(group.permissions) ? 'Clear' : 'Select' }}
35
+ </NgbButton>
36
+ </div>
37
+
38
+ <div class="divide-y divide-ngb-border">
39
+ <label
40
+ v-for="permission in group.permissions"
41
+ :key="permissionKey(permission)"
42
+ class="grid cursor-pointer grid-cols-[1.25rem,minmax(0,1fr),auto] items-start gap-3 px-4 py-3 hover:bg-[var(--ngb-row-hover)]"
43
+ :class="disabled ? 'cursor-not-allowed opacity-70' : ''"
44
+ >
45
+ <input
46
+ type="checkbox"
47
+ class="mt-1 h-4 w-4 rounded-none border-ngb-border"
48
+ :checked="selectedKeys.has(permissionKey(permission))"
49
+ :disabled="disabled"
50
+ @change="setPermission(permission, ($event.target as HTMLInputElement).checked)"
51
+ />
52
+ <span class="min-w-0">
53
+ <span class="block text-sm font-medium text-ngb-text">{{ permission.displayName }}</span>
54
+ <span v-if="permission.description" class="mt-0.5 block text-xs leading-5 text-ngb-muted">{{ permission.description }}</span>
55
+ <span class="mt-1 block truncate font-mono text-[11px] text-ngb-muted">{{ permissionKey(permission) }}</span>
56
+ </span>
57
+ <span class="mt-0.5 rounded-[var(--ngb-radius)] border border-ngb-border px-2 py-0.5 text-[11px] text-ngb-muted">
58
+ {{ permission.actionCode }}
59
+ </span>
60
+ </label>
61
+ </div>
62
+ </section>
63
+ </div>
64
+ </template>
65
+
66
+ <script setup lang="ts">
67
+ import { computed, ref } from 'vue'
68
+ import NgbButton from '../primitives/NgbButton.vue'
69
+ import NgbIcon from '../primitives/NgbIcon.vue'
70
+ import NgbInput from '../primitives/NgbInput.vue'
71
+ import { buildPermissionKey, groupPermissionDefinitions } from './permissions'
72
+ import type { PermissionAssignmentDto, PermissionDefinitionDto } from './types'
73
+
74
+ const props = withDefaults(defineProps<{
75
+ modelValue: PermissionAssignmentDto[]
76
+ definitions: PermissionDefinitionDto[]
77
+ disabled?: boolean
78
+ }>(), {
79
+ disabled: false,
80
+ })
81
+
82
+ const emit = defineEmits<{
83
+ (e: 'update:modelValue', value: PermissionAssignmentDto[]): void
84
+ }>()
85
+
86
+ const query = ref('')
87
+
88
+ const selectedKeys = computed(() => new Set((props.modelValue ?? []).map((entry) => buildPermissionKey(entry))))
89
+ const selectedCount = computed(() => selectedKeys.value.size)
90
+
91
+ const filteredDefinitions = computed(() => {
92
+ const text = query.value.trim().toLowerCase()
93
+ const definitions = props.definitions ?? []
94
+ if (!text) return definitions
95
+
96
+ return definitions.filter((permission) => [
97
+ permission.displayName,
98
+ permission.description ?? '',
99
+ permission.group,
100
+ permission.resourceKind,
101
+ permission.resourceCode,
102
+ permission.actionCode,
103
+ buildPermissionKey(permission),
104
+ ].join(' ').toLowerCase().includes(text))
105
+ })
106
+
107
+ const groups = computed(() => groupPermissionDefinitions(filteredDefinitions.value))
108
+
109
+ function permissionKey(permission: PermissionAssignmentDto): string {
110
+ return buildPermissionKey(permission)
111
+ }
112
+
113
+ function normalizeAssignments(keys: Set<string>): PermissionAssignmentDto[] {
114
+ const byKey = new Map(props.definitions.map((definition) => [permissionKey(definition), definition]))
115
+ return Array.from(keys)
116
+ .sort()
117
+ .map((key) => byKey.get(key))
118
+ .filter((entry): entry is PermissionDefinitionDto => !!entry)
119
+ .map((entry) => ({
120
+ resourceKind: entry.resourceKind,
121
+ resourceCode: entry.resourceCode,
122
+ actionCode: entry.actionCode,
123
+ }))
124
+ }
125
+
126
+ function setPermission(permission: PermissionAssignmentDto, checked: boolean): void {
127
+ if (props.disabled) return
128
+
129
+ const keys = new Set(selectedKeys.value)
130
+ const key = permissionKey(permission)
131
+ if (checked) keys.add(key)
132
+ else keys.delete(key)
133
+
134
+ emit('update:modelValue', normalizeAssignments(keys))
135
+ }
136
+
137
+ function isGroupFullySelected(permissions: PermissionDefinitionDto[]): boolean {
138
+ return permissions.length > 0 && permissions.every((permission) => selectedKeys.value.has(permissionKey(permission)))
139
+ }
140
+
141
+ function selectedInGroup(permissions: PermissionDefinitionDto[]): number {
142
+ return permissions.filter((permission) => selectedKeys.value.has(permissionKey(permission))).length
143
+ }
144
+
145
+ function toggleGroup(permissions: PermissionDefinitionDto[]): void {
146
+ if (props.disabled) return
147
+
148
+ const keys = new Set(selectedKeys.value)
149
+ const selected = isGroupFullySelected(permissions)
150
+
151
+ for (const permission of permissions) {
152
+ const key = permissionKey(permission)
153
+ if (selected) keys.delete(key)
154
+ else keys.add(key)
155
+ }
156
+
157
+ emit('update:modelValue', normalizeAssignments(keys))
158
+ }
159
+ </script>
160
+