@it-enterprise/forcebpm-ui-kit 1.0.2-beta.21 → 1.0.2-beta.22

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.
@@ -0,0 +1,121 @@
1
+ <script setup>
2
+ // FTruncate
3
+ import { ref, useTemplateRef, watch, useAttrs } from 'vue'
4
+ import { useElementSize } from '@vueuse/core'
5
+
6
+ const props = defineProps({
7
+ text: {
8
+ type: [String, Number],
9
+ default: ''
10
+ },
11
+ textClass: {
12
+ type: String,
13
+ default: ''
14
+ },
15
+ textStyle: {
16
+ type: String,
17
+ default: ''
18
+ },
19
+ lines: {
20
+ type: [String, Number],
21
+ default: 1,
22
+ validator: value => {
23
+ const num = Number(value)
24
+ return Number.isInteger(num) && num > 0
25
+ }
26
+ },
27
+ isHtml: {
28
+ type: Boolean,
29
+ default: false
30
+ }
31
+ })
32
+
33
+ const truncateRef = useTemplateRef('truncate-text')
34
+ const { width } = useElementSize(truncateRef)
35
+ const disabled = ref(true)
36
+ const menu = ref(false)
37
+ const attrs = useAttrs()
38
+ const isMouseLeave = ref(true)
39
+
40
+ const mouseoverHandler = () => {
41
+ if (isMouseLeave.value) isMouseLeave.value = false
42
+ if (disabled.value) return
43
+ setTimeout(() => {
44
+ if (isMouseLeave.value) return
45
+ menu.value = true
46
+ }, attrs['open-delay'] || 300)
47
+ }
48
+
49
+ const mouseleaveHandler = () => {
50
+ if (disabled.value) return
51
+ menu.value = false
52
+ isMouseLeave.value = true
53
+ }
54
+
55
+ watch(
56
+ [() => width.value, () => props.text],
57
+ () => {
58
+ if (!truncateRef.value) return
59
+
60
+ const element = truncateRef.value
61
+ const style = window.getComputedStyle(element)
62
+ const lineHeight = parseFloat(style.lineHeight)
63
+ const maxHeight = lineHeight * Number(props.lines)
64
+
65
+ // Checking if text needs to be truncated
66
+ disabled.value = element.scrollHeight <= Math.round(maxHeight) + 1
67
+ },
68
+ { immediate: true }
69
+ )
70
+ </script>
71
+
72
+ <template>
73
+ <v-menu
74
+ v-model="menu"
75
+ open-on-hover
76
+ z-index="1005"
77
+ offset="0, 6"
78
+ location="right"
79
+ max-height="500px"
80
+ content-class="f-truncate-menu-content"
81
+ :disabled="disabled"
82
+ >
83
+ <template #activator="{ props: activatorProps }">
84
+ <div
85
+ v-bind="activatorProps"
86
+ ref="truncate-text"
87
+ class="f-truncate"
88
+ :class="textClass"
89
+ :style="`-webkit-line-clamp: ${lines}; ${textStyle}`"
90
+ @mouseenter="mouseoverHandler"
91
+ @mouseleave="mouseleaveHandler"
92
+ >
93
+ <FHtmlContent v-if="isHtml" :data="text" />
94
+ <template v-else>{{ text }}</template>
95
+ </div>
96
+ </template>
97
+
98
+ <FHtmlContent v-if="isHtml" :data="text" />
99
+ <div v-else>{{ text }}</div>
100
+ </v-menu>
101
+ </template>
102
+
103
+ <style lang="scss" scoped>
104
+ .f-truncate {
105
+ line-height: 1.1em;
106
+ display: block;
107
+ overflow: hidden;
108
+ text-overflow: ellipsis;
109
+ display: -webkit-box;
110
+ -webkit-box-orient: vertical;
111
+ cursor: inherit;
112
+ }
113
+ :deep(.f-truncate-menu-content) {
114
+ background: rgb(var(--v-theme-background));
115
+ line-height: 1em;
116
+ font-size: 0.9em;
117
+ color: rgb(var(--v-theme-subTitle));
118
+ padding: 8px;
119
+ overflow: auto;
120
+ }
121
+ </style>
@@ -0,0 +1,195 @@
1
+ <script setup>
2
+ // FUserRoles
3
+ import { useTemplateRef, watch, ref, nextTick } from 'vue'
4
+ import { useElementSize, useParentElement } from '@vueuse/core'
5
+
6
+ const props = defineProps({
7
+ userActions: {
8
+ type: Array,
9
+ required: true
10
+ },
11
+ statusIcon: {
12
+ type: Boolean,
13
+ default: false
14
+ },
15
+ color: {
16
+ type: String,
17
+ default: 'subTitle'
18
+ }
19
+ })
20
+
21
+ const userRolesRef = useTemplateRef('userRoles')
22
+ const parentEl = useParentElement(userRolesRef)
23
+ const { width } = useElementSize(parentEl)
24
+ const count = ref(props.userActions.length)
25
+ const actionsWidth = ref([])
26
+
27
+ const statuses = [
28
+ 'COMPLETED',
29
+ 'ISSUED',
30
+ 'RUNNING',
31
+ 'CHECKING',
32
+ 'CANCELED',
33
+ 'REJECTED',
34
+ 'RETURNED_TO_INITIATOR',
35
+ 'WAITING',
36
+ 'DELEGATED',
37
+ 'PROCESSED_BY_ASSISTANT'
38
+ ]
39
+
40
+ const isStatusIcon = action => {
41
+ return props.statusIcon && action.status && statuses.includes(action.status)
42
+ }
43
+
44
+ const calculateActionsWidth = () => {
45
+ props.userActions.forEach((action, idx) => {
46
+ const virtualActionEl = document.createElement('div')
47
+ virtualActionEl.style.visibility = 'hidden'
48
+ virtualActionEl.style.position = 'absolute'
49
+ virtualActionEl.className = 'action'
50
+ virtualActionEl.style.whiteSpace = 'nowrap'
51
+ virtualActionEl.style.maxWidth = 'max-content'
52
+
53
+ if (isStatusIcon(action)) {
54
+ const iconEl = document.createElement('i')
55
+ iconEl.className = 'mr-1'
56
+ iconEl.style.display = 'inline-flex'
57
+ iconEl.style.minWidth = '6px'
58
+ iconEl.style.minHeight = '6px'
59
+ virtualActionEl.appendChild(iconEl)
60
+ }
61
+
62
+ const spanEl = document.createElement('span')
63
+ spanEl.innerText = action.identity.identityName
64
+ virtualActionEl.appendChild(spanEl)
65
+
66
+ if (idx < props.userActions.length - 1) {
67
+ const nbspSpanEl = document.createElement('span')
68
+ nbspSpanEl.innerHTML = ',&nbsp;'
69
+ virtualActionEl.appendChild(nbspSpanEl)
70
+ }
71
+
72
+ document.body.appendChild(virtualActionEl)
73
+ actionsWidth.value.push(virtualActionEl.clientWidth)
74
+ nextTick(() => {
75
+ document.body.removeChild(virtualActionEl)
76
+ })
77
+ })
78
+ }
79
+
80
+ watch(width, () => {
81
+ let totalWidth = 0
82
+ let availableWidth = width.value - 20 // 20px for menu and padding
83
+ let tempCount = 0
84
+
85
+ for (let i = 0; i < actionsWidth.value.length; i++) {
86
+ totalWidth += actionsWidth.value[i]
87
+ if (totalWidth < availableWidth) {
88
+ tempCount++
89
+ } else {
90
+ break
91
+ }
92
+ }
93
+
94
+ count.value = tempCount > 0 ? tempCount : 1
95
+ })
96
+
97
+ watch(
98
+ () => props.userActions,
99
+ () => calculateActionsWidth(),
100
+ {
101
+ immediate: true
102
+ }
103
+ )
104
+ </script>
105
+
106
+ <template>
107
+ <div ref="userRoles" class="d-flex f-user-roles">
108
+ <!-- Many users -->
109
+ <template v-if="userActions.length >= count">
110
+ <div v-for="(a, idx) in userActions.slice(0, count)" :key="a.identity.identityId" class="action" :class="`text-${color}`">
111
+ <i v-if="isStatusIcon(a)" class="status-icon mr-1" :class="a.status"></i>
112
+ <span>{{ a.identity.identityName }}</span>
113
+ <span v-if="idx < userActions.length - 1">
114
+ {{ idx !== userActions.slice(0, count).length - 1 ? ',&nbsp;' : '' }}
115
+ </span>
116
+ </div>
117
+
118
+ <v-menu
119
+ v-if="userActions.length > count"
120
+ open-on-hover
121
+ offset="2, 0"
122
+ location="right"
123
+ open-delay="500"
124
+ min-width="100"
125
+ max-width="300"
126
+ content-class="user-actions-content"
127
+ :close-on-content-click="false"
128
+ >
129
+ <template #activator="{ props: activatorProps }">
130
+ <div v-bind="activatorProps" class="font-weight-semibold cursor-pointer f-user-roles-count ml-1" :class="`text-${color}`">
131
+ +{{ userActions.length - count }}
132
+ </div>
133
+ </template>
134
+
135
+ <v-virtual-scroll :items="userActions.slice(count, userActions.length)" item-key="identity.identityId" max-height="400">
136
+ <template #default="{ item }">
137
+ <i v-if="isStatusIcon(item)" class="status-icon mr-1" :class="item.status"></i>
138
+ <FTruncate :text="item.identity.identityName" :text-class="`text-${color}`" />
139
+ </template>
140
+ </v-virtual-scroll>
141
+ </v-menu>
142
+ </template>
143
+
144
+ <!-- One user -->
145
+ <template v-else>
146
+ <div v-if="userActions[0]" class="action mr-2" :class="`text-${color}`">
147
+ <i v-if="isStatusIcon(userActions[0])" class="status-icon" :class="userActions[0].status"></i>
148
+ {{ userActions[0].identity.identityName }}
149
+ </div>
150
+ </template>
151
+ </div>
152
+ </template>
153
+
154
+ <style lang="scss" scoped>
155
+ .f-user-roles,
156
+ :deep(.f-user-actions-content) {
157
+ width: max-content;
158
+ .action {
159
+ white-space: nowrap;
160
+ }
161
+ .v-virtual-scroll {
162
+ scrollbar-width: thin;
163
+ padding: 4px 8px;
164
+ .v-virtual-scroll__item {
165
+ display: flex;
166
+ align-items: center;
167
+ }
168
+ }
169
+ .status-icon {
170
+ display: inline-flex;
171
+ min-height: 6px;
172
+ min-width: 6px;
173
+ height: 6px;
174
+ width: 6px;
175
+ border-radius: 50%;
176
+ margin-bottom: 2px;
177
+ }
178
+ .COMPLETED {
179
+ background: rgb(var(--v-theme-success));
180
+ }
181
+ .RUNNING,
182
+ .ISSUED,
183
+ .CHECKING,
184
+ .RETURNED_TO_INITIATOR,
185
+ .WAITING,
186
+ .DELEGATED,
187
+ .PROCESSED_BY_ASSISTANT {
188
+ background: rgb(var(--v-theme-info));
189
+ }
190
+ .CANCELED,
191
+ .REJECTED {
192
+ background: rgb(var(--v-theme-danger));
193
+ }
194
+ }
195
+ </style>
@@ -0,0 +1,322 @@
1
+ <script setup>
2
+ // FDatePicker
3
+ import { ref, computed, watch } from 'vue'
4
+ import moment from 'moment'
5
+
6
+ const props = defineProps({
7
+ disabled: {
8
+ type: Boolean,
9
+ default: false
10
+ },
11
+ isRange: {
12
+ type: Boolean,
13
+ default: false
14
+ },
15
+ isActions: {
16
+ type: Boolean,
17
+ default: false
18
+ },
19
+ allowedDates: {
20
+ type: Function,
21
+ default: () => true
22
+ },
23
+ type: {
24
+ type: String,
25
+ default: 'date',
26
+ validator(value, props) {
27
+ if (!['date', 'datetime'].includes(value)) return false
28
+
29
+ if (value === 'datetime' && props.isRange) {
30
+ console.warn('type "datetime" cannot be used with isRange')
31
+ return false
32
+ }
33
+ return true
34
+ }
35
+ }
36
+ })
37
+
38
+ const modelValue = defineModel({
39
+ type: [String, Date, Object],
40
+ default: null
41
+ })
42
+
43
+ const emit = defineEmits(['cancel', 'save'])
44
+
45
+ const date = ref(null)
46
+ const time = ref(null)
47
+
48
+ const FORMAT_DATE = 'YYYY-MM-DD'
49
+
50
+ const fromDateText = computed(() => {
51
+ if (props.isRange && modelValue?.value?.from) {
52
+ return moment(modelValue.value.from).format('DD.MM.YYYY')
53
+ }
54
+ return null
55
+ })
56
+
57
+ const toDateText = computed(() => {
58
+ if (props.isRange && modelValue?.value?.to) {
59
+ return moment(modelValue.value.to).format('DD.MM.YYYY')
60
+ }
61
+ return null
62
+ })
63
+
64
+ const saveDisabled = computed(() => {
65
+ if (props.isRange) {
66
+ return !(fromDateText.value && toDateText.value)
67
+ } else {
68
+ return !date.value
69
+ }
70
+ })
71
+
72
+ watch(
73
+ modelValue,
74
+ () => {
75
+ if ((typeof modelValue.value === 'string' || modelValue.value instanceof Date) && moment(modelValue.value).isValid()) {
76
+ date.value = moment(modelValue.value).format(FORMAT_DATE)
77
+ if (props.type === 'datetime') {
78
+ time.value = moment(modelValue.value).format('HH:mm')
79
+ }
80
+ return
81
+ }
82
+
83
+ if (props.isRange && modelValue.value && typeof modelValue.value === 'object' && modelValue.value.from && modelValue.value.to) {
84
+ const datesArray = []
85
+ const current = moment(modelValue.value.from).clone()
86
+ const end = moment(modelValue.value.to)
87
+
88
+ while (current.isSameOrBefore(end)) {
89
+ datesArray.push(new Date(current))
90
+ current.add(1, 'day')
91
+ }
92
+
93
+ date.value = datesArray
94
+ return
95
+ }
96
+
97
+ date.value = null
98
+ if (props.type === 'datetime') time.value = null
99
+ },
100
+ { immediate: true }
101
+ )
102
+
103
+ const updateDateHandler = value => {
104
+ if (props.type === 'date') {
105
+ if (props.isRange && Array.isArray(value) && value.length) {
106
+ const from = value[0]
107
+ const to = value.length > 1 ? value[value.length - 1] : moment(from).endOf('day').toDate()
108
+ modelValue.value = { from, to }
109
+ } else {
110
+ modelValue.value = value instanceof Date ? value : null
111
+ }
112
+ }
113
+
114
+ if (props.type === 'datetime') {
115
+ if (!time.value) time.value = '00:00'
116
+ const dateTimeString = `${moment(date.value).format(FORMAT_DATE)} ${time.value}`
117
+ modelValue.value = dateTimeString
118
+ }
119
+ }
120
+ </script>
121
+
122
+ <template>
123
+ <v-date-picker
124
+ v-model="date"
125
+ hide-title
126
+ show-adjacent-months
127
+ color="secondary"
128
+ first-day-of-week="1"
129
+ weekday-format="short"
130
+ class="f-date-picker"
131
+ mode-icon="f-icon:chevron-down bg-secondary"
132
+ next-icon="f-icon:chevron-right bg-secondary"
133
+ prev-icon="f-icon:chevron-left bg-secondary"
134
+ :hide-header="!isRange"
135
+ :allowed-dates="allowedDates"
136
+ :multiple="isRange ? 'range' : false"
137
+ @update:model-value="updateDateHandler"
138
+ >
139
+ <!-- Header for "is-range" -->
140
+ <template v-if="isRange" #header>
141
+ <div class="d-flex align-center justify-space-between f-date-picker-header">
142
+ <span class="range-title">{{ $t('pickers.period.name') }}:</span>
143
+
144
+ <!-- From date -->
145
+ <div v-if="fromDateText" class="d-flex align-center">
146
+ <span class="range-from">{{ $t('pickers.from') }}</span>
147
+ <span class="range-from-date">{{ fromDateText }}</span>
148
+ </div>
149
+
150
+ <!-- To date -->
151
+ <div v-if="toDateText" class="d-flex align-center">
152
+ <span class="range-to ml-auto">{{ $t('pickers.to') }}</span>
153
+ <span class="range-to-date">{{ toDateText }}</span>
154
+ </div>
155
+ </div>
156
+ </template>
157
+
158
+ <!-- Footer -->
159
+ <template v-if="type === 'datetime' || isActions" #actions>
160
+ <div class="d-flex flex-column fill-width f-date-picker-footer">
161
+ <!-- Time picker -->
162
+ <div v-if="type === 'datetime'" class="d-flex mt-5 font-weight-semibold f-date-picker-footer-time">
163
+ <span class="mr-2">{{ $t('pickers.time') }}:</span>
164
+ <v-menu offset="0, -6" location="bottom right" content-class="time-picker-content" :close-on-content-click="false">
165
+ <template #activator="{ props: activatorProps }">
166
+ <div class="time pointer" v-bind="activatorProps">{{ time }}</div>
167
+ </template>
168
+
169
+ <v-time-picker v-model="time" hide-title scrollable format="24hr" @update:model-value="updateDateHandler" />
170
+ </v-menu>
171
+ </div>
172
+
173
+ <!-- Picker actions -->
174
+ <div v-if="isActions" class="d-flex justify-space-between fill-width mt-5 f-date-picker-actions">
175
+ <v-btn width="48%" variant="outlined" @click="emit('cancel')">
176
+ {{ $t('buttons.cancel') }}
177
+ </v-btn>
178
+ <v-btn width="48%" :disabled="saveDisabled" variant="elevated" @click="emit('save')">
179
+ {{ $t('buttons.save') }}
180
+ </v-btn>
181
+ </div>
182
+ </div>
183
+ </template>
184
+ </v-date-picker>
185
+ </template>
186
+
187
+ <style lang="scss" scoped>
188
+ .f-date-picker {
189
+ .f-date-picker-header {
190
+ padding: 14px 14px 10px;
191
+ background-color: rgb(var(--v-theme-background));
192
+ border-bottom: 0.5px solid rgb(var(--v-theme-line));
193
+ .range-title {
194
+ font-size: 0.9em;
195
+ font-weight: 600;
196
+ line-height: 1.5em;
197
+ color: rgb(var(--v-theme-secondary));
198
+ }
199
+ .range-from,
200
+ .range-to {
201
+ font-size: 0.8em;
202
+ margin-right: 4px;
203
+ color: rgb(var(--v-theme-text));
204
+ }
205
+ .range-from-date,
206
+ .range-to-date {
207
+ font-size: 0.85em;
208
+ border-radius: 5.8px;
209
+ line-height: 1.5em;
210
+ padding-inline: 5px;
211
+ color: rgb(var(--v-theme-text));
212
+ background-color: rgb(var(--v-theme-fields));
213
+ }
214
+ }
215
+
216
+ &.v-picker.v-sheet {
217
+ border-radius: inherit;
218
+ box-shadow: none;
219
+ }
220
+ &.v-date-picker {
221
+ :deep(.v-picker__body) {
222
+ .v-date-picker-month__days {
223
+ .v-date-picker-month__day {
224
+ .v-btn.v-btn--variant-text .v-btn__overlay {
225
+ background: rgb(var(--v-theme-secondary));
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ :deep(.v-picker__actions) {
233
+ padding-bottom: 40px;
234
+ border-top: 1px solid rgb(var(--v-theme-line));
235
+ justify-content: flex-start;
236
+ .v-btn {
237
+ margin-inline-end: 0;
238
+ }
239
+ }
240
+ .f-date-picker-footer {
241
+ .f-date-picker-footer-time {
242
+ color: rgb(var(--v-theme-secondary));
243
+ .time {
244
+ &::after {
245
+ content: '';
246
+ display: block;
247
+ width: 100%;
248
+ height: 0;
249
+ border-bottom: 0.5px solid rgb(var(--v-theme-secondary));
250
+ position: relative;
251
+ top: -1px;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ .time-picker-content {
258
+ .v-time-picker {
259
+ min-width: 290px;
260
+ :deep(.v-picker__header) {
261
+ padding: 10px 20px 4px;
262
+ background: rgb(var(--v-theme-background-dark));
263
+ .v-time-picker-controls {
264
+ .v-time-picker-controls__time__field {
265
+ height: auto;
266
+ .v-field {
267
+ font-size: 32px;
268
+ background-color: rgb(var(--v-theme-fields));
269
+ .v-field__input {
270
+ padding: 4px 8px;
271
+ min-height: 20px;
272
+ color: rgb(var(--v-theme-fields-light));
273
+ }
274
+ }
275
+ }
276
+ .v-time-picker-controls__field-label {
277
+ opacity: 1;
278
+ }
279
+ .v-time-picker-controls__time__separator {
280
+ font-size: 32px;
281
+ line-height: 44px;
282
+ color: rgb(var(--v-theme-fields-light));
283
+ }
284
+ }
285
+ }
286
+ :deep(.v-picker__body) {
287
+ .v-time-picker-clock {
288
+ margin: 20px;
289
+ background: rgb(var(--v-theme-line));
290
+ .v-time-picker-clock__hand {
291
+ background: rgb(var(--v-theme-secondary));
292
+ &::before {
293
+ border-color: rgb(var(--v-theme-secondary));
294
+ }
295
+ &::after {
296
+ background: rgb(var(--v-theme-secondary));
297
+ }
298
+ }
299
+ .v-time-picker-clock__item {
300
+ color: rgb(var(--v-theme-text));
301
+ font-size: 14px;
302
+ height: 28px;
303
+ width: 28px;
304
+ &.v-time-picker-clock__item--active {
305
+ color: rgb(var(--v-theme-fields));
306
+ background: rgb(var(--v-theme-secondary));
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ .v-theme--dark {
314
+ .time-picker-content {
315
+ .v-time-picker {
316
+ :deep(.v-picker__header) {
317
+ border-bottom: 1px solid rgb(var(--v-theme-line));
318
+ }
319
+ }
320
+ }
321
+ }
322
+ </style>