@it-enterprise/forcebpm-ui-kit 1.0.2-beta.25 → 1.0.2-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/FDialog.vue +2 -0
- package/src/FSearchPanel.vue +3 -0
- package/src/FUserGroupPicker.vue +630 -0
- package/src/assets/scss/input.scss +25 -3
- package/src/assets/scss/tabs.scss +3 -3
- package/src/assets/scss/utilities.scss +27 -5
- package/src/locales/en.json +13 -1
- package/src/locales/ru.json +14 -1
- package/src/locales/uk.json +14 -1
- package/src/utils/color.js +5 -0
package/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { default as FUserRoles } from './src/FUserRoles.vue'
|
|
|
25
25
|
export { default as FDatePicker } from './src/f-date-picker/FDatePicker.vue'
|
|
26
26
|
export { default as FMenuDatePicker } from './src/f-date-picker/FMenuDatePicker.vue'
|
|
27
27
|
export { default as FTextFieldDate } from './src/f-date-picker/FTextFieldDate.vue'
|
|
28
|
-
|
|
28
|
+
export { default as FUserGroupPicker } from './src/FUserGroupPicker.vue'
|
|
29
29
|
|
|
30
30
|
// Utils
|
|
31
31
|
export { hexToRGBA, createRadialGradient } from './src/utils/color.js'
|
package/package.json
CHANGED
package/src/FDialog.vue
CHANGED
package/src/FSearchPanel.vue
CHANGED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// FUserGroupPicker
|
|
3
|
+
import { ref, computed, watch } from 'vue'
|
|
4
|
+
import { useI18n } from 'vue-i18n'
|
|
5
|
+
import { useTheme } from 'vuetify'
|
|
6
|
+
import { colorCollection, hexToRGBA } from './utils/color'
|
|
7
|
+
|
|
8
|
+
const { t } = useI18n()
|
|
9
|
+
const theme = useTheme()
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
items: {
|
|
13
|
+
type: Array,
|
|
14
|
+
required: true,
|
|
15
|
+
validator: (value, props) => {
|
|
16
|
+
const isValid = value.every(item => {
|
|
17
|
+
const hasIdentityId =
|
|
18
|
+
item[props.itemValue] !== undefined && (typeof item[props.itemValue] === 'string' || typeof item[props.itemValue] === 'number')
|
|
19
|
+
const hasName = typeof item.name === 'string'
|
|
20
|
+
const isUserOrGroup = item.type === 'USER' || item.type === 'GROUP' || item.groupId !== undefined
|
|
21
|
+
return hasIdentityId && hasName && isUserOrGroup
|
|
22
|
+
})
|
|
23
|
+
if (!isValid) {
|
|
24
|
+
console.warn('FUserGroupPicker: Each item should have identityId (string or number), name (string) and type "USER" or "GROUP" (or groupId)')
|
|
25
|
+
}
|
|
26
|
+
return isValid
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
itemValue: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: 'identityId',
|
|
32
|
+
validator: (value, props) => {
|
|
33
|
+
const hasGroups = props.items?.some(item => item.type === 'GROUP' || item.groupId)
|
|
34
|
+
if (hasGroups && value === 'userId') {
|
|
35
|
+
console.warn('FUserGroupPicker: itemValue cannot be "userId" when items contain groups. Use "identityId" instead.')
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
if (!['identityId', 'userId'].includes(value)) {
|
|
39
|
+
console.warn('FUserGroupPicker: itemValue should be one of identityId, userId')
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
// todo when there will be a connection in ForceBPMTasks
|
|
46
|
+
executorLists: {
|
|
47
|
+
type: Array,
|
|
48
|
+
default: () => []
|
|
49
|
+
},
|
|
50
|
+
multiple: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: false
|
|
53
|
+
},
|
|
54
|
+
returnObject: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: false
|
|
57
|
+
},
|
|
58
|
+
groupSelection: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: false
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const state = defineModel('state', {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const modelValue = defineModel('modelValue', {
|
|
70
|
+
type: [Array, Object, String, Number],
|
|
71
|
+
default: () => []
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const search = ref('')
|
|
75
|
+
const selectedMenuId = ref(null)
|
|
76
|
+
const tab = ref('USERS_BY_GROUP')
|
|
77
|
+
const calculatedHeight = Math.floor(window.innerHeight * 0.1) // 10vh rounded down
|
|
78
|
+
|
|
79
|
+
// Tabs for header
|
|
80
|
+
const tabs = computed(() => {
|
|
81
|
+
const baseTabs = [
|
|
82
|
+
{ value: 'USERS_BY_GROUP', text: t('user.usersByGroup') },
|
|
83
|
+
{ value: 'USERS_BY_POSITION', text: t('user.usersByPosition') }
|
|
84
|
+
]
|
|
85
|
+
if (props.executorLists.length > 0) {
|
|
86
|
+
baseTabs.push({ value: 'EXECUTOR_LISTS', text: t('user.executorLists') })
|
|
87
|
+
}
|
|
88
|
+
return baseTabs
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Map for quick access by identityId - O(1)
|
|
92
|
+
const itemsMap = computed(() => {
|
|
93
|
+
const map = new Map()
|
|
94
|
+
for (const item of props.items) {
|
|
95
|
+
map.set(item.identityId, item)
|
|
96
|
+
}
|
|
97
|
+
return map
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Map userId -> identityId for executor lists lookup
|
|
101
|
+
const userIdMap = computed(() => {
|
|
102
|
+
const map = new Map()
|
|
103
|
+
for (const item of props.items) {
|
|
104
|
+
if (item.userId !== undefined) {
|
|
105
|
+
map.set(item.userId, item.identityId)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return map
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Unique groups from items (extracted from item.groups)
|
|
112
|
+
const uniqueGroups = computed(() => {
|
|
113
|
+
const groupsMap = new Map()
|
|
114
|
+
for (const item of props.items) {
|
|
115
|
+
if (item.groups?.length) {
|
|
116
|
+
for (const group of item.groups) {
|
|
117
|
+
if (!groupsMap.has(group.identityId)) {
|
|
118
|
+
groupsMap.set(group.identityId, { ...group, id: group.identityId })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Array.from(groupsMap.values())
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Unique positions from items
|
|
127
|
+
const uniquePositions = computed(() => {
|
|
128
|
+
const positionsMap = new Map()
|
|
129
|
+
for (const item of props.items) {
|
|
130
|
+
if (item.positionCode && !positionsMap.has(item.positionCode)) {
|
|
131
|
+
positionsMap.set(item.positionCode, {
|
|
132
|
+
id: item.positionCode,
|
|
133
|
+
title: item.positionTitle
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return Array.from(positionsMap.values())
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Index: groupId -> Set<itemValue> for quick filtering
|
|
141
|
+
const groupIndex = computed(() => {
|
|
142
|
+
const index = new Map()
|
|
143
|
+
for (const item of props.items) {
|
|
144
|
+
if (item.groups?.length) {
|
|
145
|
+
for (const group of item.groups) {
|
|
146
|
+
if (!index.has(group.identityId)) {
|
|
147
|
+
index.set(group.identityId, new Set())
|
|
148
|
+
}
|
|
149
|
+
index.get(group.identityId).add(item.identityId)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return index
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Index: positionCode -> Set<itemValue>
|
|
157
|
+
const positionIndex = computed(() => {
|
|
158
|
+
const index = new Map()
|
|
159
|
+
for (const item of props.items) {
|
|
160
|
+
if (item.positionCode) {
|
|
161
|
+
if (!index.has(item.positionCode)) {
|
|
162
|
+
index.set(item.positionCode, new Set())
|
|
163
|
+
}
|
|
164
|
+
index.get(item.positionCode).add(item.identityId)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return index
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Index: executorListId -> Set<identityId>
|
|
171
|
+
const executorIndex = computed(() => {
|
|
172
|
+
const index = new Map()
|
|
173
|
+
for (const list of props.executorLists) {
|
|
174
|
+
const userIds = new Set()
|
|
175
|
+
for (const userId of list.usersIds || []) {
|
|
176
|
+
const identityId = userIdMap.value.get(userId)
|
|
177
|
+
if (identityId) {
|
|
178
|
+
userIds.add(identityId)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
index.set(list.id, userIds)
|
|
182
|
+
}
|
|
183
|
+
return index
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Set selected items - O(1) for checking
|
|
187
|
+
const selectedIds = ref(new Set())
|
|
188
|
+
|
|
189
|
+
// Set selected groups (when groupSelection = true)
|
|
190
|
+
const selectedGroupIds = ref(new Set())
|
|
191
|
+
|
|
192
|
+
const menuItems = computed(() => {
|
|
193
|
+
const color = idx => {
|
|
194
|
+
return hexToRGBA(colorCollection[theme.global.name.value === 'light' ? 'light' : 'dark'][idx % colorCollection.light.length]).slice(5, -3) // rgba(255, 0, 0, 0.2) -> 255, 0, 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
switch (tab.value) {
|
|
198
|
+
case 'USERS_BY_GROUP':
|
|
199
|
+
return uniqueGroups.value.map((g, idx) => ({
|
|
200
|
+
id: g.identityId,
|
|
201
|
+
name: g.name,
|
|
202
|
+
color: color(idx)
|
|
203
|
+
}))
|
|
204
|
+
case 'USERS_BY_POSITION':
|
|
205
|
+
return uniquePositions.value.map((p, idx) => ({
|
|
206
|
+
id: p.id,
|
|
207
|
+
name: p.title,
|
|
208
|
+
color: color(idx)
|
|
209
|
+
}))
|
|
210
|
+
case 'EXECUTOR_LISTS':
|
|
211
|
+
return props.executorLists.map((list, idx) => ({
|
|
212
|
+
id: list.id,
|
|
213
|
+
name: list.name,
|
|
214
|
+
color: color(idx)
|
|
215
|
+
}))
|
|
216
|
+
default:
|
|
217
|
+
return []
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const titleByMenu = computed(() => {
|
|
222
|
+
const currentMenu = menuItems.value.find(i => i.id === selectedMenuId.value)
|
|
223
|
+
return currentMenu?.name || currentMenu?.id || (props.groupSelection ? t('user.allUsersAndGroups') : t('user.allUsers'))
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const filteredItems = computed(() => {
|
|
227
|
+
let resultIds = null
|
|
228
|
+
|
|
229
|
+
// Step 1: Get relevant item IDs based on selected menu
|
|
230
|
+
if (selectedMenuId.value) {
|
|
231
|
+
switch (tab.value) {
|
|
232
|
+
case 'USERS_BY_GROUP':
|
|
233
|
+
resultIds = groupIndex.value.get(selectedMenuId.value)
|
|
234
|
+
break
|
|
235
|
+
case 'USERS_BY_POSITION':
|
|
236
|
+
resultIds = positionIndex.value.get(selectedMenuId.value)
|
|
237
|
+
break
|
|
238
|
+
case 'EXECUTOR_LISTS':
|
|
239
|
+
resultIds = executorIndex.value.get(selectedMenuId.value)
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 2: Map IDs to items (if resultIds is null, we take all items)
|
|
245
|
+
let result
|
|
246
|
+
if (resultIds) {
|
|
247
|
+
result = Array.from(resultIds, id => itemsMap.value.get(id)).filter(Boolean)
|
|
248
|
+
} else {
|
|
249
|
+
result = props.items
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 3: Apply search filter
|
|
253
|
+
if (search.value) {
|
|
254
|
+
result = result.filter(item => item.name?.toLowerCase().includes(search.value.toLowerCase()))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 4: If groupSelection is enabled, mark items as disabled if they belong to a selected group
|
|
258
|
+
if (props.groupSelection && selectedGroupIds.value.size > 0) {
|
|
259
|
+
result = result.map(item => {
|
|
260
|
+
const isDisabledByGroup = item.groups?.some(g => selectedGroupIds.value.has(g.identityId))
|
|
261
|
+
return isDisabledByGroup ? { ...item, disabled: true } : item
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Selected items (groups + users)
|
|
269
|
+
const selectedItems = computed(() => {
|
|
270
|
+
// Users — filter out those covered by a selected group
|
|
271
|
+
const users = Array.from(selectedIds.value, id => itemsMap.value.get(id))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.filter(user => {
|
|
274
|
+
if (props.groupSelection && selectedGroupIds.value.size > 0 && user.groups?.length) {
|
|
275
|
+
return !user.groups.some(g => selectedGroupIds.value.has(g.identityId))
|
|
276
|
+
}
|
|
277
|
+
return true
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// Groups — with selectAll flag for removeSelected logic
|
|
281
|
+
const groups = Array.from(selectedGroupIds.value, groupId => {
|
|
282
|
+
const group = uniqueGroups.value.find(g => g.identityId === groupId)
|
|
283
|
+
if (group) {
|
|
284
|
+
return {
|
|
285
|
+
...group,
|
|
286
|
+
type: 'GROUP',
|
|
287
|
+
selectAll: true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null
|
|
291
|
+
}).filter(Boolean)
|
|
292
|
+
|
|
293
|
+
const result = [...groups, ...users]
|
|
294
|
+
|
|
295
|
+
if (search.value) {
|
|
296
|
+
return result.filter(item => item.name?.toLowerCase().includes(search.value.toLowerCase()))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return result
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const isSelected = item => {
|
|
303
|
+
// Check if user is individually selected
|
|
304
|
+
if (selectedIds.value.has(item.identityId)) return true
|
|
305
|
+
|
|
306
|
+
// Check if user is selected via group
|
|
307
|
+
if (props.groupSelection && item.groups?.some(g => selectedGroupIds.value.has(g.identityId))) {
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const toggleSelect = item => {
|
|
315
|
+
if (item.disabled) return
|
|
316
|
+
|
|
317
|
+
const id = item.identityId
|
|
318
|
+
|
|
319
|
+
if (props.multiple) {
|
|
320
|
+
if (selectedIds.value.has(id)) {
|
|
321
|
+
selectedIds.value.delete(id)
|
|
322
|
+
} else {
|
|
323
|
+
selectedIds.value.add(id)
|
|
324
|
+
}
|
|
325
|
+
// Trigger reactivity
|
|
326
|
+
selectedIds.value = new Set(selectedIds.value)
|
|
327
|
+
} else {
|
|
328
|
+
// Single selection - immediately close picker
|
|
329
|
+
selectedIds.value = new Set([id])
|
|
330
|
+
selectHandler()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const removeSelected = item => {
|
|
335
|
+
if (item.disabled) return
|
|
336
|
+
|
|
337
|
+
if (item.selectAll) {
|
|
338
|
+
selectedGroupIds.value.delete(item.identityId)
|
|
339
|
+
selectedGroupIds.value = new Set(selectedGroupIds.value)
|
|
340
|
+
} else {
|
|
341
|
+
selectedIds.value.delete(item.identityId)
|
|
342
|
+
selectedIds.value = new Set(selectedIds.value)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const toggleAllInMenu = () => {
|
|
347
|
+
if (!selectedMenuId.value) return
|
|
348
|
+
|
|
349
|
+
if (props.groupSelection && tab.value === 'USERS_BY_GROUP') {
|
|
350
|
+
if (selectedGroupIds.value.has(selectedMenuId.value)) {
|
|
351
|
+
// Deselect group
|
|
352
|
+
selectedGroupIds.value.delete(selectedMenuId.value)
|
|
353
|
+
selectedGroupIds.value = new Set(selectedGroupIds.value)
|
|
354
|
+
} else {
|
|
355
|
+
// Select group
|
|
356
|
+
selectedGroupIds.value.add(selectedMenuId.value)
|
|
357
|
+
selectedGroupIds.value = new Set(selectedGroupIds.value)
|
|
358
|
+
|
|
359
|
+
// Remove individually selected users from this group
|
|
360
|
+
const groupUserIds = groupIndex.value.get(selectedMenuId.value)
|
|
361
|
+
if (groupUserIds) {
|
|
362
|
+
for (const userId of groupUserIds) {
|
|
363
|
+
selectedIds.value.delete(userId)
|
|
364
|
+
}
|
|
365
|
+
selectedIds.value = new Set(selectedIds.value)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
let ids
|
|
370
|
+
switch (tab.value) {
|
|
371
|
+
case 'USERS_BY_GROUP':
|
|
372
|
+
ids = groupIndex.value.get(selectedMenuId.value)
|
|
373
|
+
break
|
|
374
|
+
case 'USERS_BY_POSITION':
|
|
375
|
+
ids = positionIndex.value.get(selectedMenuId.value)
|
|
376
|
+
break
|
|
377
|
+
case 'EXECUTOR_LISTS':
|
|
378
|
+
ids = executorIndex.value.get(selectedMenuId.value)
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!ids) return
|
|
383
|
+
|
|
384
|
+
if (isAllSelectedInMenu.value) {
|
|
385
|
+
// Deselect all
|
|
386
|
+
for (const id of ids) {
|
|
387
|
+
selectedIds.value.delete(id)
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
// Select all (skip disabled)
|
|
391
|
+
for (const id of ids) {
|
|
392
|
+
const item = itemsMap.value.get(id)
|
|
393
|
+
if (item && !item.disabled) {
|
|
394
|
+
selectedIds.value.add(id)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
selectedIds.value = new Set(selectedIds.value)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Check if all items in current menu are selected (taking into account groupSelection logic)
|
|
403
|
+
const isAllSelectedInMenu = computed(() => {
|
|
404
|
+
if (!selectedMenuId.value) return false
|
|
405
|
+
|
|
406
|
+
if (props.groupSelection && tab.value === 'USERS_BY_GROUP') {
|
|
407
|
+
return selectedGroupIds.value.has(selectedMenuId.value)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let ids
|
|
411
|
+
switch (tab.value) {
|
|
412
|
+
case 'USERS_BY_GROUP':
|
|
413
|
+
ids = groupIndex.value.get(selectedMenuId.value)
|
|
414
|
+
break
|
|
415
|
+
case 'USERS_BY_POSITION':
|
|
416
|
+
ids = positionIndex.value.get(selectedMenuId.value)
|
|
417
|
+
break
|
|
418
|
+
case 'EXECUTOR_LISTS':
|
|
419
|
+
ids = executorIndex.value.get(selectedMenuId.value)
|
|
420
|
+
break
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!ids || ids.size === 0) return false
|
|
424
|
+
|
|
425
|
+
for (const id of ids) {
|
|
426
|
+
if (!selectedIds.value.has(id)) return false
|
|
427
|
+
}
|
|
428
|
+
return true
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const selectMenuItem = id => {
|
|
432
|
+
selectedMenuId.value = selectedMenuId.value === id ? null : id
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const open = () => {
|
|
436
|
+
state.value = true
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const close = () => {
|
|
440
|
+
state.value = false
|
|
441
|
+
search.value = ''
|
|
442
|
+
selectedMenuId.value = null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const selectHandler = () => {
|
|
446
|
+
if (props.returnObject) {
|
|
447
|
+
modelValue.value = selectedItems.value
|
|
448
|
+
} else {
|
|
449
|
+
modelValue.value = selectedItems.value.map(item => item[props.itemValue])
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
state.value = false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Reset menu selection when switching tabs
|
|
456
|
+
watch(tab, () => {
|
|
457
|
+
selectedMenuId.value = null
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// Sync with external modelValue changes
|
|
461
|
+
watch(state, newState => {
|
|
462
|
+
if (newState && modelValue.value) {
|
|
463
|
+
// Reset
|
|
464
|
+
selectedIds.value = new Set()
|
|
465
|
+
selectedGroupIds.value = new Set()
|
|
466
|
+
|
|
467
|
+
// Populate from modelValue
|
|
468
|
+
const values = Array.isArray(modelValue.value) ? modelValue.value : [modelValue.value]
|
|
469
|
+
for (const val of values) {
|
|
470
|
+
if (props.returnObject) {
|
|
471
|
+
if (val.type === 'GROUP' || val.groupId) {
|
|
472
|
+
selectedGroupIds.value.add(val.identityId)
|
|
473
|
+
} else {
|
|
474
|
+
selectedIds.value.add(val.identityId)
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// Find item by itemValue field
|
|
478
|
+
let found = false
|
|
479
|
+
for (const item of props.items) {
|
|
480
|
+
if (item[props.itemValue] === val) {
|
|
481
|
+
if (item.type === 'GROUP' || item.groupId) {
|
|
482
|
+
selectedGroupIds.value.add(item.identityId)
|
|
483
|
+
} else {
|
|
484
|
+
selectedIds.value.add(item.identityId)
|
|
485
|
+
}
|
|
486
|
+
found = true
|
|
487
|
+
break
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// If not found in items, try uniqueGroups
|
|
491
|
+
if (!found && uniqueGroups.value.some(g => g[props.itemValue] === val)) {
|
|
492
|
+
const group = uniqueGroups.value.find(g => g[props.itemValue] === val)
|
|
493
|
+
selectedGroupIds.value.add(group.identityId)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
</script>
|
|
500
|
+
<template>
|
|
501
|
+
<FDialog
|
|
502
|
+
v-model:search="search"
|
|
503
|
+
:model-value="state"
|
|
504
|
+
name="userGroupPicker"
|
|
505
|
+
width="80vw"
|
|
506
|
+
max-width="900px"
|
|
507
|
+
persistent
|
|
508
|
+
no-space
|
|
509
|
+
is-search
|
|
510
|
+
:title="$t('user.executors')"
|
|
511
|
+
@open="open"
|
|
512
|
+
@close="close"
|
|
513
|
+
>
|
|
514
|
+
<div class="user-group-picker">
|
|
515
|
+
<!-- Header tabs -->
|
|
516
|
+
<v-tabs v-model="tab" grow class="mb-2">
|
|
517
|
+
<v-tab v-for="tabItem in tabs" :key="tabItem.value" :value="tabItem.value" height="35px" :text="tabItem.text" />
|
|
518
|
+
</v-tabs>
|
|
519
|
+
|
|
520
|
+
<!-- Filter menu -->
|
|
521
|
+
<v-tabs-window v-model="tab" class="mx-5">
|
|
522
|
+
<v-tabs-window-item
|
|
523
|
+
v-for="tabItem in tabs"
|
|
524
|
+
:key="tabItem.value"
|
|
525
|
+
:value="tabItem.value"
|
|
526
|
+
:style="{ height: `${calculatedHeight}px` }"
|
|
527
|
+
class="f-scrollbar-y"
|
|
528
|
+
>
|
|
529
|
+
<v-chip-group selected-class="active" class="pa-0" column>
|
|
530
|
+
<v-chip
|
|
531
|
+
v-for="item in menuItems"
|
|
532
|
+
:key="item.id"
|
|
533
|
+
:style="`--f-user-group-chip-color: ${item.color}`"
|
|
534
|
+
variant="flat"
|
|
535
|
+
class="user-group-picker-chip"
|
|
536
|
+
@click="selectMenuItem(item.id)"
|
|
537
|
+
>
|
|
538
|
+
{{ item.name || item.id }}
|
|
539
|
+
</v-chip>
|
|
540
|
+
</v-chip-group>
|
|
541
|
+
</v-tabs-window-item>
|
|
542
|
+
</v-tabs-window>
|
|
543
|
+
|
|
544
|
+
<div class="d-flex flex-column mx-5 mt-5">
|
|
545
|
+
<!-- Title -->
|
|
546
|
+
<span class="mb-2 ml-1 font-weight-semibold fs-11">{{ titleByMenu }}</span>
|
|
547
|
+
|
|
548
|
+
<!-- Select all users -->
|
|
549
|
+
<v-checkbox
|
|
550
|
+
v-if="selectedMenuId && multiple && (filteredItems.length || selectedItems.length)"
|
|
551
|
+
class="mb-2 ml-1"
|
|
552
|
+
:model-value="isAllSelectedInMenu"
|
|
553
|
+
:label="props.groupSelection && tab === 'USERS_BY_GROUP' ? $t('user.selectGroup') : $t('user.selectAllFromList')"
|
|
554
|
+
@update:model-value="toggleAllInMenu"
|
|
555
|
+
/>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div v-if="filteredItems.length || selectedItems.length" class="d-flex justify-space-between mx-5">
|
|
559
|
+
<!-- Available items -->
|
|
560
|
+
<v-virtual-scroll :items="filteredItems" height="40vh" item-key="identityId" item-height="40" max-width="45%" class="pr-2">
|
|
561
|
+
<template #default="{ item }">
|
|
562
|
+
<div class="d-flex align-center justify-start picker-list-item" :class="{ 'is-disabled': item.disabled }" @click="toggleSelect(item)">
|
|
563
|
+
<v-checkbox :model-value="isSelected(item)" :disabled="item.disabled" />
|
|
564
|
+
<FAvatar :full-user="item.type === 'USER' ? item : null" :group="item.type === 'GROUP' ? item : null" size="20" class="ml-1 mr-2" />
|
|
565
|
+
<span class="picker-item-name">{{ item.name }}</span>
|
|
566
|
+
</div>
|
|
567
|
+
</template>
|
|
568
|
+
</v-virtual-scroll>
|
|
569
|
+
|
|
570
|
+
<!-- Selected items -->
|
|
571
|
+
<v-virtual-scroll :items="selectedItems" height="40vh" item-key="identityId" item-height="40" max-width="45%" class="pr-2">
|
|
572
|
+
<template #default="{ item }">
|
|
573
|
+
<div class="d-flex align-center justify-start picker-list-item" :class="{ 'is-disabled': item.disabled }" @click="removeSelected(item)">
|
|
574
|
+
<v-checkbox :model-value="true" :disabled="item.disabled" />
|
|
575
|
+
<FAvatar :full-user="item.type === 'USER' ? item : null" :group="item.type === 'GROUP' ? item : null" size="20" class="ml-1 mr-2" />
|
|
576
|
+
<span class="picker-item-name">{{ item.name }}</span>
|
|
577
|
+
</div>
|
|
578
|
+
</template>
|
|
579
|
+
</v-virtual-scroll>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<FNoData v-else height="40vh" class="mt-5" :is-search="search" />
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<template #actions>
|
|
586
|
+
<v-btn class="mx-auto" width="300" @click="selectHandler">
|
|
587
|
+
{{ $t('buttons.select') }}
|
|
588
|
+
</v-btn>
|
|
589
|
+
</template>
|
|
590
|
+
</FDialog>
|
|
591
|
+
</template>
|
|
592
|
+
|
|
593
|
+
<style lang="scss" scoped>
|
|
594
|
+
.user-group-picker {
|
|
595
|
+
:deep(.v-virtual-scroll__item) {
|
|
596
|
+
padding: 0;
|
|
597
|
+
&:nth-child(odd) {
|
|
598
|
+
background-color: rgb(var(--v-theme-fields));
|
|
599
|
+
}
|
|
600
|
+
.picker-list-item {
|
|
601
|
+
padding: 5px;
|
|
602
|
+
transition: all 0.3s ease-in-out;
|
|
603
|
+
&:not(.is-disabled) {
|
|
604
|
+
cursor: pointer;
|
|
605
|
+
&:hover {
|
|
606
|
+
background-color: rgba(var(--v-theme-primary), 0.2);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
.user-group-picker-chip {
|
|
612
|
+
--v-chip-height: 20px;
|
|
613
|
+
margin: 3px;
|
|
614
|
+
border-radius: 4.8px;
|
|
615
|
+
color: rgb(var(--f-user-group-chip-color));
|
|
616
|
+
background: rgba(var(--f-user-group-chip-color), 0.2);
|
|
617
|
+
border: 1px solid transparent;
|
|
618
|
+
&.active {
|
|
619
|
+
border-color: rgb(var(--f-user-group-chip-color));
|
|
620
|
+
}
|
|
621
|
+
&.v-theme--dark {
|
|
622
|
+
color: rgb(var(--v-theme-title));
|
|
623
|
+
background: rgba(var(--f-user-group-chip-color), 0.7);
|
|
624
|
+
&.active {
|
|
625
|
+
border-color: rgb(var(--v-theme-title));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
</style>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Override input styles
|
|
2
2
|
.f-input {
|
|
3
3
|
border-radius: 5.8px;
|
|
4
|
-
background-color: rgb(var(--v-theme-background));
|
|
5
4
|
color: rgb(var(--v-theme-text));
|
|
6
|
-
|
|
5
|
+
.v-input__control {
|
|
6
|
+
background-color: rgb(var(--v-theme-fields));
|
|
7
|
+
}
|
|
7
8
|
&:hover {
|
|
8
9
|
.v-field {
|
|
9
10
|
.v-label {
|
|
@@ -140,6 +141,26 @@
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
// Autocomplete User
|
|
145
|
+
.f-input.f-autocomplete-user {
|
|
146
|
+
// .v-field {
|
|
147
|
+
// .v-field__outline__end {
|
|
148
|
+
// min-width: calc(100% - 53px);
|
|
149
|
+
// }
|
|
150
|
+
// }
|
|
151
|
+
&.v-input--horizontal .v-input__append {
|
|
152
|
+
margin-inline-start: 0px;
|
|
153
|
+
.f-icon {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
transition: all 0.3s ease-in-out;
|
|
156
|
+
&:hover {
|
|
157
|
+
background: rgb(var(--v-theme-primary)) !important;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Textarea
|
|
143
164
|
.f-input.v-textarea {
|
|
144
165
|
.v-field {
|
|
145
166
|
.v-field__input {
|
|
@@ -170,6 +191,7 @@
|
|
|
170
191
|
border-radius: 16.8px;
|
|
171
192
|
font-size: 0.75em;
|
|
172
193
|
background: rgb(var(--v-theme-line));
|
|
194
|
+
color: rgb(var(--v-theme-subTitle));
|
|
173
195
|
.v-chip__close {
|
|
174
196
|
margin-inline-start: 5px;
|
|
175
197
|
margin-inline-end: 5px;
|
|
@@ -242,11 +264,11 @@
|
|
|
242
264
|
|
|
243
265
|
//Radio
|
|
244
266
|
.f-radio {
|
|
245
|
-
--v-selection-control-size: 16px;
|
|
246
267
|
&.v-selection-control--inline:not(:last-child) {
|
|
247
268
|
margin-right: 8px;
|
|
248
269
|
}
|
|
249
270
|
.v-selection-control__wrapper {
|
|
271
|
+
--v-selection-control-size: 16px;
|
|
250
272
|
.v-selection-control__input {
|
|
251
273
|
&::before {
|
|
252
274
|
content: none;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
height: 1px;
|
|
16
16
|
width: 100%;
|
|
17
17
|
background: rgb(var(--v-theme-line));
|
|
18
|
-
transform: scaleY(
|
|
18
|
+
transform: scaleY(1);
|
|
19
19
|
transform-origin: bottom;
|
|
20
20
|
}
|
|
21
21
|
.v-slide-group__content {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
}
|
|
30
30
|
.v-btn__content {
|
|
31
31
|
.v-tab__slider {
|
|
32
|
-
height:
|
|
33
|
-
|
|
32
|
+
// height: 0;
|
|
33
|
+
// opacity: 0;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -63,19 +63,41 @@
|
|
|
63
63
|
&::-webkit-scrollbar {
|
|
64
64
|
width: 6px;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
&::-webkit-scrollbar-track {
|
|
67
|
+
border-radius: 10.8px;
|
|
68
|
+
box-shadow: inset 0 0 6px rgba(var(--v-theme-disabled));
|
|
69
|
+
}
|
|
67
70
|
&::-webkit-scrollbar-thumb {
|
|
68
|
-
|
|
71
|
+
border-top: 0px solid rgba(0, 0, 0, 0);
|
|
72
|
+
border-bottom: 0px solid rgba(0, 0, 0, 0);
|
|
73
|
+
background-clip: padding-box;
|
|
69
74
|
border-radius: 10.8px;
|
|
70
|
-
|
|
75
|
+
height: 50px;
|
|
76
|
+
background-color: rgba(var(--v-theme-disabled));
|
|
71
77
|
&:hover {
|
|
72
|
-
background: rgba(var(--v-theme-primary)
|
|
78
|
+
background-color: rgba(var(--v-theme-primary));
|
|
73
79
|
}
|
|
74
80
|
}
|
|
81
|
+
}
|
|
75
82
|
|
|
83
|
+
.v-virtual-scroll {
|
|
84
|
+
&::-webkit-scrollbar {
|
|
85
|
+
width: 6px;
|
|
86
|
+
}
|
|
76
87
|
&::-webkit-scrollbar-track {
|
|
77
|
-
background: rgba(var(--v-theme-disabled), 0.4);
|
|
78
88
|
border-radius: 10.8px;
|
|
89
|
+
box-shadow: inset 0 0 6px rgba(var(--v-theme-disabled));
|
|
90
|
+
}
|
|
91
|
+
&::-webkit-scrollbar-thumb {
|
|
92
|
+
border-top: 0px solid rgba(0, 0, 0, 0);
|
|
93
|
+
border-bottom: 0px solid rgba(0, 0, 0, 0);
|
|
94
|
+
background-clip: padding-box;
|
|
95
|
+
border-radius: 10.8px;
|
|
96
|
+
height: 50px;
|
|
97
|
+
background-color: rgba(var(--v-theme-disabled));
|
|
98
|
+
&:hover {
|
|
99
|
+
background-color: rgba(var(--v-theme-primary));
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
package/src/locales/en.json
CHANGED
|
@@ -53,7 +53,19 @@
|
|
|
53
53
|
"user": {
|
|
54
54
|
"telephone": "Phone number",
|
|
55
55
|
"position": "Position",
|
|
56
|
-
"department": "Department"
|
|
56
|
+
"department": "Department",
|
|
57
|
+
"executor": "Executor",
|
|
58
|
+
"executors": "Executors",
|
|
59
|
+
"initiator": "Initiator",
|
|
60
|
+
"candidates": "Candidates",
|
|
61
|
+
"inform": "To attention",
|
|
62
|
+
"executorLists": "Executor lists",
|
|
63
|
+
"usersByGroup": "Users by groups",
|
|
64
|
+
"usersByPosition": "Users by job title",
|
|
65
|
+
"allUsers": "All users",
|
|
66
|
+
"allUsersAndGroups": "All users and groups",
|
|
67
|
+
"selectGroup": "Select group",
|
|
68
|
+
"selectAllFromList": "Select all from list"
|
|
57
69
|
},
|
|
58
70
|
"tooltip": {
|
|
59
71
|
"actions": "Additional actions",
|
package/src/locales/ru.json
CHANGED
|
@@ -53,7 +53,20 @@
|
|
|
53
53
|
"user": {
|
|
54
54
|
"telephone": "Номер телефона",
|
|
55
55
|
"position": "Должность",
|
|
56
|
-
"department": "Департамент"
|
|
56
|
+
"department": "Департамент",
|
|
57
|
+
"executor": "Исполнитель",
|
|
58
|
+
"executors": "Исполнители",
|
|
59
|
+
"candidate": "Кандидат",
|
|
60
|
+
"candidates": "Кандидаты",
|
|
61
|
+
"initiator": "Инициатор",
|
|
62
|
+
"inform": "К сведению",
|
|
63
|
+
"executorLists": "Списки исполнителей",
|
|
64
|
+
"usersByGroup": "Пользователи по группам",
|
|
65
|
+
"usersByPosition": "Пользователи по должностям",
|
|
66
|
+
"allUsers": "Все пользователи",
|
|
67
|
+
"allUsersAndGroups": "Все пользователи и группы",
|
|
68
|
+
"selectGroup": "Выбрать группу",
|
|
69
|
+
"selectAllFromList": "Выбрать всех из списка"
|
|
57
70
|
},
|
|
58
71
|
"tooltip": {
|
|
59
72
|
"actions": "Дополнительные действия",
|
package/src/locales/uk.json
CHANGED
|
@@ -53,7 +53,20 @@
|
|
|
53
53
|
"user": {
|
|
54
54
|
"telephone": "Номер телефону",
|
|
55
55
|
"position": "Посада",
|
|
56
|
-
"department": "Департамент"
|
|
56
|
+
"department": "Департамент",
|
|
57
|
+
"executor": "Виконавець",
|
|
58
|
+
"executors": "Виконавці",
|
|
59
|
+
"candidate": "Кандидат",
|
|
60
|
+
"candidates": "Кандидати",
|
|
61
|
+
"initiator": "Ініціатор",
|
|
62
|
+
"inform": "До відома",
|
|
63
|
+
"executorLists": "Списки виконавців",
|
|
64
|
+
"usersByGroup": "Користувачі за групами",
|
|
65
|
+
"usersByPosition": "Користувачі за посадою",
|
|
66
|
+
"allUsers": "Всі користувачі",
|
|
67
|
+
"allUsersAndGroups": "Всі користувачі та групи",
|
|
68
|
+
"selectGroup": "Вибрати групу",
|
|
69
|
+
"selectAllFromList": "Вибрати всіх зі списку"
|
|
57
70
|
},
|
|
58
71
|
"tooltip": {
|
|
59
72
|
"actions": "Додаткові дії",
|
package/src/utils/color.js
CHANGED
|
@@ -31,3 +31,8 @@ export const hexToRGBA = (hex, alpha) => {
|
|
|
31
31
|
export const createRadialGradient = (mainColor, spreadColor) => {
|
|
32
32
|
return `radial-gradient(83.34% 88.78% at 50.00% 50.00%, ${hexToRGBA(mainColor, 0.25)} 0%, ${hexToRGBA(mainColor, 0.08)} 31.56%, ${hexToRGBA(spreadColor, 0)} 57.76%)`
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
export const colorCollection = {
|
|
36
|
+
light: ['#5400A1', '#038D00', '#0051DC', '#EE6D10', '#DC005C', '#7400CF', '#006794', '#0B14EF', '#CA00DC', '#007B5E'],
|
|
37
|
+
dark: ['#8848C5', '#6A9269', '#6E96DB', '#D45496', '#0081B9', '#D1AD55', '#DC7B7B', '#614FCF', '#007B5E', '#AF3EB9']
|
|
38
|
+
}
|