@webitel/ui-datalist 26.12.5 → 26.12.7

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 (47) hide show
  1. package/Readme.md +18 -0
  2. package/package.json +4 -4
  3. package/src/index.ts +5 -2
  4. package/src/modules/filters/modules/filterConfig/components/grantee/index.ts +1 -1
  5. package/src/modules/filters/modules/filterConfig/components/index.ts +8 -0
  6. package/src/modules/filters/modules/filterConfig/components/trigger-job-state/trigger-job-state-filter-value-field.vue +87 -0
  7. package/src/modules/filters/modules/filterConfig/components/trigger-job-state/trigger-job-state-filter-value-preview.vue +42 -0
  8. package/src/modules/filters/modules/filterConfig/enums/FilterOption.ts +2 -0
  9. package/src/modules/headers/__tests__/createTableHeadersStore.spec.ts +80 -0
  10. package/src/modules/headers/createTableHeadersStore.ts +51 -11
  11. package/src/modules/permissions/components/_internal/permissions-role-row.vue +25 -0
  12. package/src/modules/permissions/components/_internal/permissions-role-select.vue +35 -0
  13. package/src/modules/permissions/components/_internal/permissions-tab-content.vue +161 -0
  14. package/src/modules/permissions/components/_internal/permissions-tab-role-popup.vue +94 -0
  15. package/src/modules/{permissions-page → permissions}/components/permissions-tab.vue +5 -5
  16. package/src/modules/permissions/composables/useAccessModeOptions.ts +48 -0
  17. package/src/modules/permissions/index.ts +40 -0
  18. package/src/modules/permissions/stores/_internals/headers.ts +52 -0
  19. package/src/modules/{permissions-page → permissions}/stores/createPermissionsStore.ts +19 -44
  20. package/src/modules/{permissions-page → permissions}/types/Permission.types.ts +2 -2
  21. package/src/modules/permissions/types/PermissionsUserAccess.ts +10 -0
  22. package/types/.tsbuildinfo +1 -1
  23. package/types/index.d.ts +2 -2
  24. package/types/modules/filter-presets/stores/createFilterPresetsStore.d.ts +1 -1
  25. package/types/modules/filters/modules/filterConfig/components/grantee/index.d.ts +1 -1
  26. package/types/modules/filters/modules/filterConfig/components/index.d.ts +4 -2
  27. package/types/modules/filters/modules/filterConfig/components/trigger-job-state/trigger-job-state-filter-value-field.vue.d.ts +18 -0
  28. package/types/modules/filters/modules/filterConfig/components/trigger-job-state/trigger-job-state-filter-value-preview.vue.d.ts +6 -0
  29. package/types/modules/filters/modules/filterConfig/enums/FilterOption.d.ts +2 -0
  30. package/types/modules/filters/scripts/variableFilters.d.ts +1 -0
  31. package/types/modules/headers/createTableHeadersStore.d.ts +6 -0
  32. package/types/modules/permissions/components/_internal/permissions-role-row.vue.d.ts +17 -0
  33. package/types/modules/permissions/components/_internal/permissions-role-select.vue.d.ts +11 -0
  34. package/types/modules/permissions/components/_internal/permissions-tab-content.vue.d.ts +32 -0
  35. package/types/modules/permissions/components/_internal/permissions-tab-role-popup.vue.d.ts +10 -0
  36. package/types/modules/{permissions-page → permissions}/components/permissions-tab.vue.d.ts +1 -1
  37. package/types/modules/permissions/composables/useAccessModeOptions.d.ts +22 -0
  38. package/types/modules/permissions/index.d.ts +17 -0
  39. package/types/modules/permissions/stores/_internals/headers.d.ts +22 -0
  40. package/types/modules/{permissions-page → permissions}/stores/createPermissionsStore.d.ts +5 -3
  41. package/types/modules/{permissions-page → permissions}/types/Permission.types.d.ts +2 -2
  42. package/types/modules/permissions/types/PermissionsUserAccess.d.ts +3 -0
  43. package/src/modules/permissions-page/index.ts +0 -23
  44. package/src/modules/permissions-page/scripts/PermissionsApiModule.ts +0 -57
  45. package/src/modules/permissions-page/scripts/__tests__/PermissionsApiModule.spec.ts +0 -31
  46. package/types/modules/permissions-page/index.d.ts +0 -7
  47. package/types/modules/permissions-page/scripts/PermissionsApiModule.d.ts +0 -9
package/Readme.md CHANGED
@@ -24,6 +24,7 @@ This package contains:
24
24
  * card page tools (store, components, etc)
25
25
  * table filters (store, components, etc)
26
26
  * table filter presets (store, components, etc)
27
+ * permissions tab (store, components, etc)
27
28
 
28
29
  ## Exports
29
30
 
@@ -85,6 +86,23 @@ import {
85
86
  } from '@webitel/ui-datalist/card';
86
87
  ```
87
88
 
89
+ ### Permissions tools
90
+
91
+ Object ACL tab of an entity card. See docs: `pages/packages/ui-datalist/permissions`.
92
+
93
+ #### Import
94
+
95
+ ```ts
96
+ import {
97
+ createPermissionsStore, // pinia store factory, takes the entity api (getPermissionsList / patchPermissions)
98
+ PermissionsTab, // ready tab: register it in the router, pass store / access / parentId
99
+ headersWithCreate, // columns with "create" (x), for ACLs that have it
100
+ PermissionsRoleRow, // role with its role / user icon
101
+ PermissionsRoleSelect, // roles select
102
+ useAccessModeOptions, // access mode options + rows with localized rule names
103
+ } from '@webitel/ui-datalist/permissions';
104
+ ```
105
+
88
106
  ## Versions
89
107
 
90
108
  // TODO: describe new major/minor versions, if will be needed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-datalist",
3
- "version": "26.12.5",
3
+ "version": "26.12.7",
4
4
  "description": "Toolkit for building data lists in webitel ui system",
5
5
  "scripts": {
6
6
  "build:types": "vue-tsc -b tsconfig.build.json",
@@ -96,9 +96,9 @@
96
96
  "types": "./types/modules/card/index.d.ts",
97
97
  "import": "./src/modules/card/index.ts"
98
98
  },
99
- "./permissions-page": {
100
- "types": "./types/modules/permissions-page/index.d.ts",
101
- "import": "./src/modules/permissions-page/index.ts"
99
+ "./permissions": {
100
+ "types": "./types/modules/permissions/index.d.ts",
101
+ "import": "./src/modules/permissions/index.ts"
102
102
  }
103
103
  }
104
104
  }
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { useNestedTableList } from './modules/table/composables/useNestedTableList';
2
2
  import { createTableStore } from './modules/table/createTableStore.store';
3
- import type { DatalistTableHeader } from './modules/types/tableStore.types';
3
+ import type {
4
+ DatalistTableHeader,
5
+ PatchItemPropertyParams,
6
+ } from './modules/types/tableStore.types';
4
7
 
5
- export type { DatalistTableHeader };
8
+ export type { DatalistTableHeader, PatchItemPropertyParams };
6
9
  export { createTableStore, useNestedTableList };
@@ -1,4 +1,4 @@
1
- import RolesAPI from '@webitel/ui-sdk/api/clients/roles/roles';
1
+ import { RolesAPI } from '@webitel/api-services/api';
2
2
  import { WtObject } from '@webitel/ui-sdk/enums';
3
3
 
4
4
  import {
@@ -135,6 +135,8 @@ import TeamFilter from './team/team-filter-value-field.vue';
135
135
  import TeamFilterPreview from './team/team-filter-value-preview.vue';
136
136
  import TotalDurationFilter from './total-duration/total-duration-filter-value-field.vue';
137
137
  import TotalDurationFilterPreview from './total-duration/total-duration-filter-value-preview.vue';
138
+ import TriggerJobStateFilter from './trigger-job-state/trigger-job-state-filter-value-field.vue';
139
+ import TriggerJobStateFilterPreview from './trigger-job-state/trigger-job-state-filter-value-preview.vue';
138
140
  import { createUserFilterConfig } from './user';
139
141
  import UserFilter from './user/user-filter-value-field.vue';
140
142
  import UserFilterPreview from './user/user-filter-value-preview.vue';
@@ -247,6 +249,8 @@ export {
247
249
  TeamFilterPreview,
248
250
  TotalDurationFilter,
249
251
  TotalDurationFilterPreview,
252
+ TriggerJobStateFilter,
253
+ TriggerJobStateFilterPreview,
250
254
  UserFilter,
251
255
  UserFilterPreview,
252
256
  UtilizationProgressFilter,
@@ -291,6 +295,7 @@ export const FilterOptionToValueComponentMap: Record<FilterOption, Component> =
291
295
  [FilterOption.JoinedAt]: DateTimeOptionsFilterValueField,
292
296
  [FilterOption.LeavingAt]: DateTimeOptionsFilterValueField,
293
297
  [FilterOption.OfferingAt]: DateTimeOptionsFilterValueField,
298
+ [FilterOption.StartedAt]: DateTimeOptionsFilterValueField,
294
299
  [FilterOption.Bucket]: BucketFilter,
295
300
  [FilterOption.StopCause]: StopCauseFilter,
296
301
  [FilterOption.MemberPriority]: RatingFromToFilter,
@@ -299,6 +304,7 @@ export const FilterOptionToValueComponentMap: Record<FilterOption, Component> =
299
304
  [FilterOption.MemberName]: StringFilterValueField,
300
305
  [FilterOption.MemberDestination]: StringFilterValueField,
301
306
  [FilterOption.CallReportingResult]: CallReportingResultFilter,
307
+ [FilterOption.TriggerJobState]: TriggerJobStateFilter,
302
308
  [FilterOption.Action]: ActionFilter,
303
309
  [FilterOption.Object]: ObjectFilter,
304
310
  [FilterOption.CaseStatus]: CaseStatusFilterValueField,
@@ -329,6 +335,7 @@ export const FilterOptionToPreviewComponentMap: Record<
329
335
  [FilterOption.JoinedAt]: DateTimeOptionsFilterValuePreview,
330
336
  [FilterOption.LeavingAt]: DateTimeOptionsFilterValuePreview,
331
337
  [FilterOption.OfferingAt]: DateTimeOptionsFilterValuePreview,
338
+ [FilterOption.StartedAt]: DateTimeOptionsFilterValuePreview,
332
339
  [FilterOption.Bucket]: BucketFilterPreview,
333
340
  [FilterOption.StopCause]: StopCauseFilterPreview,
334
341
  [FilterOption.MemberPriority]: RatingFromToFilterPreview,
@@ -337,6 +344,7 @@ export const FilterOptionToPreviewComponentMap: Record<
337
344
  [FilterOption.MemberName]: StringFilterValuePreview,
338
345
  [FilterOption.MemberDestination]: StringFilterValuePreview,
339
346
  [FilterOption.CallReportingResult]: CallReportingResultFilterPreview,
347
+ [FilterOption.TriggerJobState]: TriggerJobStateFilterPreview,
340
348
  [FilterOption.Action]: ActionFilterPreview,
341
349
  [FilterOption.Object]: ObjectFilterPreview,
342
350
  [FilterOption.Agent]: AgentFilterPreview,
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <wt-multi-select
3
+ :label="labelValue"
4
+ :options="options"
5
+ v-model:model-value="model"
6
+ :v="!disableValidation && v$.model"
7
+ chips-view
8
+ data-key="value"
9
+ option-label="label"
10
+ option-value="value"
11
+ />
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { useVuelidate } from '@vuelidate/core';
16
+ import { required } from '@vuelidate/validators';
17
+ import { EngineTriggerJobState } from '@webitel/api-services/gen/models';
18
+ import { WtMultiSelect } from '@webitel/ui-sdk/components';
19
+ import { computed, onMounted, watch } from 'vue';
20
+ import { useI18n } from 'vue-i18n';
21
+
22
+ import { WtSysTypeFilterConfig } from '../../classes/FilterConfig';
23
+
24
+ const model = defineModel<string[]>();
25
+ const { t } = useI18n();
26
+
27
+ const props = defineProps<{
28
+ filterConfig?: WtSysTypeFilterConfig;
29
+ disableValidation?: boolean;
30
+ }>();
31
+
32
+ const v$ = useVuelidate(
33
+ computed(() => ({
34
+ model: {
35
+ required,
36
+ },
37
+ })),
38
+ {
39
+ model,
40
+ },
41
+ {
42
+ $autoDirty: true,
43
+ },
44
+ );
45
+
46
+ v$.value.$touch();
47
+
48
+ const emit = defineEmits<{
49
+ 'update:invalid': [
50
+ boolean,
51
+ ];
52
+ }>();
53
+
54
+ const labelValue = computed(() =>
55
+ t(
56
+ `webitelUI.filters.${
57
+ props?.filterConfig?.showFilterName
58
+ ? props?.filterConfig.name
59
+ : 'filterValue'
60
+ }`,
61
+ ),
62
+ );
63
+
64
+ /** [Claude] the states a trigger run ends up in, for the trigger logs' result filter */
65
+ const options = computed(() =>
66
+ Object.values(EngineTriggerJobState).map((value) => ({
67
+ value,
68
+ label: t(`objects.triggerJobState.${value}`),
69
+ })),
70
+ );
71
+
72
+ onMounted(() => {
73
+ if (!props?.disableValidation) v$.value.$touch();
74
+ });
75
+
76
+ watch(
77
+ () => v$.value.$invalid,
78
+ (invalid) => {
79
+ emit('update:invalid', invalid);
80
+ },
81
+ {
82
+ immediate: true,
83
+ },
84
+ );
85
+ </script>
86
+
87
+ <style scoped></style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <ul>
3
+ <li
4
+ v-for="(label, index) of arrayValues"
5
+ :key="index"
6
+ >
7
+ {{ label }}
8
+ </li>
9
+ </ul>
10
+ </template>
11
+
12
+ <script lang="ts" setup>
13
+ import { EngineTriggerJobState } from '@webitel/api-services/gen/models';
14
+ import { computed } from 'vue';
15
+ import { useI18n } from 'vue-i18n';
16
+
17
+ const props = defineProps<{
18
+ value?: string[];
19
+ }>();
20
+
21
+ const { t } = useI18n();
22
+
23
+ const options = computed(() =>
24
+ Object.values(EngineTriggerJobState).map((value) => ({
25
+ value,
26
+ label: t(`objects.triggerJobState.${value}`),
27
+ })),
28
+ );
29
+
30
+ const arrayLabels = computed(() =>
31
+ options.value.reduce<Record<string, string>>((acc, { value, label }) => {
32
+ acc[value] = label;
33
+ return acc;
34
+ }, {}),
35
+ );
36
+
37
+ const arrayValues = computed(() =>
38
+ (props.value ?? []).map((v) => arrayLabels.value[v] || v),
39
+ );
40
+ </script>
41
+
42
+ <style scoped></style>
@@ -55,6 +55,7 @@ export const FilterOption = {
55
55
  JoinedAt: 'joinedAt',
56
56
  LeavingAt: 'leavingAt',
57
57
  OfferingAt: 'offeringAt',
58
+ StartedAt: 'startedAt',
58
59
  UploadedAt: 'uploadedAt',
59
60
  StopCause: 'stopCause',
60
61
  MemberPriority: 'memberPriority',
@@ -62,6 +63,7 @@ export const FilterOption = {
62
63
  MemberName: 'name',
63
64
  MemberDestination: 'destination',
64
65
  CallReportingResult: 'result' /** queue attempt CallReportingStatus */,
66
+ TriggerJobState: 'triggerJobState',
65
67
  Action: 'action',
66
68
  Object: 'object',
67
69
  Date: 'date',
@@ -146,6 +146,86 @@ describe('tableHeadersStoreBody', () => {
146
146
  });
147
147
  });
148
148
 
149
+ it('marks a resized column as resized', () => {
150
+ const store = createStore();
151
+
152
+ store.columnResize({
153
+ columnName: 'name',
154
+ columnWidth: '100px',
155
+ });
156
+
157
+ expect(store.headers.value[0]).toMatchObject({
158
+ width: '100px',
159
+ resized: true,
160
+ });
161
+ expect(store.headers.value[1].resized).toBeUndefined();
162
+ });
163
+
164
+ describe('column widths persistence', () => {
165
+ const resizedWidthsKey = `${id}/resizedColumnWidths`;
166
+ const legacyWidthsKey = `${id}/columnWidths`;
167
+
168
+ it('saves resized widths only, not default ones from header definitions', async () => {
169
+ const store = createStore();
170
+ store.headers.value[1].width = '300px'; // default width
171
+ await runWithRouter(() => store.setupPersistence());
172
+
173
+ store.columnResize({
174
+ columnName: 'name',
175
+ columnWidth: '100px',
176
+ });
177
+ await flushWrites();
178
+
179
+ expect(JSON.parse(localStorage.getItem(resizedWidthsKey) ?? '')).toEqual({
180
+ name: '100px',
181
+ });
182
+ });
183
+
184
+ it('restores saved widths as resized', async () => {
185
+ localStorage.setItem(
186
+ resizedWidthsKey,
187
+ JSON.stringify({
188
+ name: '100px',
189
+ }),
190
+ );
191
+
192
+ const store = await setUpStore();
193
+
194
+ expect(store.headers.value[0]).toMatchObject({
195
+ width: '100px',
196
+ resized: true,
197
+ });
198
+ expect(store.headers.value[1].resized).toBeUndefined();
199
+ });
200
+
201
+ it('restores legacy widths as plain widths, overridden by resized ones', async () => {
202
+ localStorage.setItem(
203
+ legacyWidthsKey,
204
+ JSON.stringify({
205
+ name: '50px',
206
+ subject: '60px',
207
+ }),
208
+ );
209
+ localStorage.setItem(
210
+ resizedWidthsKey,
211
+ JSON.stringify({
212
+ name: '100px',
213
+ }),
214
+ );
215
+
216
+ const store = await setUpStore();
217
+
218
+ expect(store.headers.value[0]).toMatchObject({
219
+ width: '100px',
220
+ resized: true,
221
+ });
222
+ expect(store.headers.value[1]).toMatchObject({
223
+ width: '60px',
224
+ resized: false,
225
+ });
226
+ });
227
+ });
228
+
149
229
  describe('persistence', () => {
150
230
  it('restores shown fields from the route query', async () => {
151
231
  await router.push({
@@ -81,6 +81,16 @@ export const tableHeadersStoreBody = ({
81
81
  }, {});
82
82
  });
83
83
 
84
+ // [Claude] widths of columns resized by the user only, default widths from header definitions are not included
85
+ const resizedColumnWidths = computed(() => {
86
+ return headers.value.reduce<Record<string, string>>((acc, header) => {
87
+ if (header.resized && header.width) {
88
+ acc[header.field] = header.width;
89
+ }
90
+ return acc;
91
+ }, {});
92
+ });
93
+
84
94
  const $reset = () => {
85
95
  headers.value = allowedHeaders;
86
96
  };
@@ -294,29 +304,58 @@ export const tableHeadersStoreBody = ({
294
304
  }),
295
305
  });
296
306
 
307
+ /*
308
+ * [Claude] stored under a new name: legacy "columnWidths" mixes resized widths with default ones,
309
+ * so it can't tell which columns were resized. It's still restored, but as plain (min) widths
310
+ */
311
+ const legacyColumnWidthsName = 'columnWidths';
312
+
313
+ const restoreWidths = async (
314
+ restore: (name: string) => Promise<unknown>,
315
+ name: string,
316
+ {
317
+ resized,
318
+ }: {
319
+ resized: boolean;
320
+ },
321
+ ) => {
322
+ const value = (await restore(name)) as string;
323
+ if (!value) return;
324
+
325
+ const parsedWidths = JSON.parse(value);
326
+ headers.value = headers.value.map((header) =>
327
+ parsedWidths[header.field]
328
+ ? {
329
+ ...header,
330
+ width: parsedWidths[header.field],
331
+ resized,
332
+ }
333
+ : header,
334
+ );
335
+ };
336
+
297
337
  const columnWidthsStorage = usePersistedStorage({
298
- name: 'columnWidths',
299
- value: columnWidths,
338
+ name: 'resizedColumnWidths',
339
+ value: resizedColumnWidths,
300
340
  storages: [
301
341
  PersistedStorageType.LocalStorage,
302
342
  ],
303
343
  storagePath: id,
304
344
  onStore: (save, { name }) => {
305
- const value = JSON.stringify(columnWidths.value);
345
+ const value = JSON.stringify(resizedColumnWidths.value);
306
346
  return save({
307
347
  name,
308
348
  value,
309
349
  });
310
350
  },
311
351
  onRestore: async (restore, name) => {
312
- const value = (await restore(name)) as string;
313
- if (value) {
314
- const parsedWidths = JSON.parse(value);
315
- headers.value = headers.value.map((header) => ({
316
- ...header,
317
- width: parsedWidths[header.field] || header.width,
318
- }));
319
- }
352
+ // [Claude] sequentially, so resized widths override legacy ones
353
+ await restoreWidths(restore, legacyColumnWidthsName, {
354
+ resized: false,
355
+ });
356
+ await restoreWidths(restore, name, {
357
+ resized: true,
358
+ });
320
359
  },
321
360
  });
322
361
 
@@ -355,6 +394,7 @@ export const tableHeadersStoreBody = ({
355
394
 
356
395
  if (column) {
357
396
  column.width = columnWidth;
397
+ column.resized = true;
358
398
  }
359
399
  };
360
400
 
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <div class="permissions-role-row">
3
+ <wt-icon :icon="role.user ? 'user' : 'role'" />
4
+ <slot>
5
+ {{ role.name }}
6
+ </slot>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import type { PermissionGrantee } from '../../types/Permission.types';
12
+
13
+ defineProps<{
14
+ role: PermissionGrantee;
15
+ }>();
16
+ </script>
17
+
18
+ <style lang="scss" scoped>
19
+ .permissions-role-row {
20
+ display: grid;
21
+ grid-template-columns: var(--icon-md-size) 1fr;
22
+ align-items: center;
23
+ gap: var(--spacing-sm);
24
+ }
25
+ </style>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <wt-single-select
3
+ class="permissions-role-select"
4
+ option-label="name"
5
+ v-bind="attrs"
6
+ v-model:model-value="model"
7
+ >
8
+ <template #value="{ value, getOptionLabel }">
9
+ <permissions-role-row :role="value">
10
+ {{ getOptionLabel(value) || value }}
11
+ </permissions-role-row>
12
+ </template>
13
+
14
+ <template #option="{ option, getOptionLabel }">
15
+ <permissions-role-row :role="option">
16
+ {{ getOptionLabel(option) || option }}
17
+ </permissions-role-row>
18
+ </template>
19
+ </wt-single-select>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { useAttrs } from 'vue';
24
+
25
+ import type { PermissionGrantee } from '../../types/Permission.types';
26
+ import PermissionsRoleRow from './permissions-role-row.vue';
27
+
28
+ const attrs = useAttrs();
29
+
30
+ const model = defineModel<PermissionGrantee | null>({
31
+ required: true,
32
+ });
33
+ </script>
34
+
35
+ <style lang="scss" scoped></style>
@@ -0,0 +1,161 @@
1
+ <template>
2
+ <section
3
+ v-if="access.read"
4
+ class="table-section"
5
+ >
6
+ <permissions-tab-role-popup
7
+ v-if="access.create"
8
+ :existing-grantee-ids="existingGranteeIds"
9
+ :add-role-permissions="addRolePermissions"
10
+ />
11
+
12
+ <header class="table-title permissions-tab-content__header">
13
+ <wt-action-bar
14
+ :include="[IconAction.ADD, IconAction.REFRESH]"
15
+ :disabled:add="!access.create"
16
+ @click:add="openRolePopup"
17
+ @click:refresh="refresh"
18
+ />
19
+ </header>
20
+
21
+ <div class="table-section__table-wrapper">
22
+ <wt-empty
23
+ v-show="showEmpty"
24
+ :image="imageEmpty"
25
+ :text="textEmpty"
26
+ />
27
+
28
+ <wt-loader v-show="isLoading" />
29
+
30
+ <div
31
+ v-show="dataList.length && !isLoading"
32
+ class="table-section__visible-scroll-wrapper"
33
+ >
34
+ <wt-table
35
+ :data="localizedDataList"
36
+ :grid-actions="access.update"
37
+ :headers="headers"
38
+ :selectable="false"
39
+ sortable
40
+ @sort="sort"
41
+ >
42
+ <template #grantee="{ item }">
43
+ <permissions-role-row :role="item.grantee" />
44
+ </template>
45
+
46
+ <!-- [Claude] one select per rule column; "create" shows only where the headers have it, e.g. object classes OBAC -->
47
+ <template
48
+ v-for="(ruleName, column) of AccessRuleColumn"
49
+ :key="column"
50
+ #[column]="{ item }"
51
+ >
52
+ <wt-single-select
53
+ :show-clear="false"
54
+ :disabled="!access.update"
55
+ :options="accessOptions"
56
+ :model-value="item.access[ruleName]"
57
+ @update:model-value="changeAccessMode({ item, ruleName, mode: $event })"
58
+ />
59
+ </template>
60
+
61
+ <template #actions="{ item }">
62
+ <wt-icon-action
63
+ action="delete"
64
+ @click="
65
+ changeAccessMode({
66
+ item,
67
+ ruleName: AccessRuleName.R,
68
+ mode: { id: AccessMode.Forbidden },
69
+ })
70
+ "
71
+ />
72
+ </template>
73
+ </wt-table>
74
+ </div>
75
+
76
+ <slot name="pagination" />
77
+ </div>
78
+ </section>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import {
83
+ AccessMode,
84
+ AccessRuleColumn,
85
+ AccessRuleName,
86
+ } from '@webitel/api-services/enums';
87
+ import { IconAction } from '@webitel/ui-sdk/enums';
88
+ import { useTableEmpty } from '@webitel/ui-sdk/src/modules/TableComponentModule/composables/useTableEmpty';
89
+ import { computed } from 'vue';
90
+ import { useRoute, useRouter } from 'vue-router';
91
+
92
+ import type { DatalistTableHeader } from '../../../types/tableStore.types';
93
+ import { useAccessModeOptions } from '../../composables/useAccessModeOptions';
94
+ import type {
95
+ ChangeAccessModePayload,
96
+ PermissionEntity,
97
+ PermissionGrantee,
98
+ } from '../../types/Permission.types';
99
+ import {
100
+ DEFAULT_PERMISSIONS_USER_ACCESS,
101
+ type PermissionsUserAccess,
102
+ } from '../../types/PermissionsUserAccess';
103
+ import PermissionsRoleRow from './permissions-role-row.vue';
104
+ import PermissionsTabRolePopup from './permissions-tab-role-popup.vue';
105
+
106
+ const props = withDefaults(
107
+ defineProps<{
108
+ dataList: PermissionEntity[];
109
+ isLoading?: boolean;
110
+ headers: DatalistTableHeader[];
111
+ error?: unknown;
112
+ refresh: () => unknown;
113
+ sort: (column: DatalistTableHeader) => unknown;
114
+ changeAccessMode: (payload: ChangeAccessModePayload) => unknown;
115
+ existingGranteeIds?: PermissionGrantee['id'][];
116
+ addRolePermissions: (role: PermissionGrantee) => Promise<unknown>;
117
+ access?: PermissionsUserAccess;
118
+ }>(),
119
+ {
120
+ isLoading: false,
121
+ existingGranteeIds: () => [],
122
+ access: () => DEFAULT_PERMISSIONS_USER_ACCESS,
123
+ },
124
+ );
125
+
126
+ const { accessOptions, localizedDataList } = useAccessModeOptions(
127
+ () => props.dataList,
128
+ );
129
+
130
+ const route = useRoute();
131
+ const router = useRouter();
132
+
133
+ /** [Claude] the role popup is opened by the route, so it survives a reload */
134
+ const openRolePopup = () =>
135
+ router.push({
136
+ name: route.name,
137
+ query: route.query,
138
+ hash: route.hash,
139
+ params: {
140
+ ...route.params,
141
+ permissionId: 'new',
142
+ },
143
+ });
144
+
145
+ const {
146
+ showEmpty,
147
+ image: imageEmpty,
148
+ text: textEmpty,
149
+ } = useTableEmpty({
150
+ dataList: computed(() => props.dataList),
151
+ error: computed(() => props.error),
152
+ isLoading: computed(() => props.isLoading),
153
+ });
154
+ </script>
155
+
156
+ <style scoped>
157
+ /* [Claude] no title in the header, so push the action bar to the right */
158
+ .permissions-tab-content__header {
159
+ justify-content: flex-end;
160
+ }
161
+ </style>