shared-ritm 1.2.56 → 1.2.58

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,17 @@
1
+ /**
2
+ * Сравнивает два значения на глубокое равенство.
3
+ * Поддерживает массивы, объекты и примитивы.
4
+ */
5
+ export declare function isEqual(a: any, b: any): boolean;
6
+ /**
7
+ * Нормализует значение:
8
+ * - Если передан массив объектов, возвращает массив `.value` или сам объект.
9
+ * - Если передан объект, возвращает `.value` или сам объект.
10
+ * - Если примитив — возвращает без изменений.
11
+ */
12
+ export declare function normalizeValue(val: any): any;
13
+ /**
14
+ * Генерирует UUID v4.
15
+ * Используется для идентификаторов временных сущностей.
16
+ */
17
+ export declare function uuidv4(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-ritm",
3
- "version": "1.2.56",
3
+ "version": "1.2.58",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -5,6 +5,7 @@
5
5
  standout
6
6
  outlined
7
7
  dense
8
+ :rules="rules"
8
9
  :type="inputType"
9
10
  autocomplete="h87h58g7h8hd"
10
11
  :readonly="readonly || field"
@@ -1,64 +1,3 @@
1
- <script setup lang="ts">
2
- import { defineProps, defineEmits, ref, computed, watch } from 'vue'
3
- import type { Ref } from 'vue'
4
- import FilterIcon from '@/icons/components/table-filter-icon.vue'
5
-
6
- interface FilterOption {
7
- id: string
8
- name: string
9
- }
10
-
11
- interface TableEmits {
12
- 'toggle-filter-value': [colName: string, value: string]
13
- 'clear-filter': [colName: string]
14
- 'open-filter-menu': [colName: string, isOpen: boolean]
15
- 'row-click': [row: Record<string, any>]
16
- 'update:selectedRows': [rows: any[]]
17
- }
18
-
19
- const emit = defineEmits<TableEmits>()
20
-
21
- const props = defineProps<{
22
- rows: Ref<any[]>
23
- columns: any[]
24
- columnFilters: Ref<Record<string, string | string[] | undefined>>
25
- filterMenus: Ref<Record<string, boolean>>
26
- filtersOptions: Record<string, FilterOption[]>
27
- meta: Ref<{ currentPage: number; perPage: number }>
28
- enableMultiSelect?: boolean
29
- selectedRows: any[]
30
- }>()
31
-
32
- const localSearches = ref<Record<string, string>>({})
33
-
34
- const filteredOptions = computed(() => {
35
- const result: Record<string, FilterOption[]> = {}
36
- for (const col of props.columns) {
37
- const search = localSearches.value[col.name]?.toLowerCase() || ''
38
- const options = props.filtersOptions[col.name] || []
39
- result[col.name] = options.filter(opt => opt.name.toLowerCase().includes(search))
40
- }
41
- return result
42
- })
43
- const selected = ref<any[]>([])
44
-
45
- watch(
46
- () => props.selectedRows,
47
- val => {
48
- selected.value = val
49
- },
50
- { immediate: true },
51
- )
52
-
53
- watch(
54
- selected,
55
- val => {
56
- emit('update:selectedRows', val)
57
- },
58
- { deep: true },
59
- )
60
- </script>
61
-
62
1
  <template>
63
2
  <q-page class="flex flex-col" style="min-height: 100%">
64
3
  <q-table
@@ -175,7 +114,65 @@ watch(
175
114
  </q-table>
176
115
  </q-page>
177
116
  </template>
117
+ <script setup lang="ts">
118
+ import { defineProps, defineEmits, ref, computed, watch } from 'vue'
119
+ import type { Ref } from 'vue'
120
+ import FilterIcon from '@/icons/components/table-filter-icon.vue'
121
+
122
+ interface FilterOption {
123
+ id: string
124
+ name: string
125
+ }
126
+
127
+ interface TableEmits {
128
+ 'toggle-filter-value': [colName: string, value: string]
129
+ 'clear-filter': [colName: string]
130
+ 'open-filter-menu': [colName: string, isOpen: boolean]
131
+ 'row-click': [row: Record<string, any>]
132
+ 'update:selectedRows': [rows: any[]] // синтаксис defineEmits для эмита с именованными параметрами
133
+ }
134
+
135
+ const props = defineProps<{
136
+ rows: Ref<any[]>
137
+ columns: any[]
138
+ columnFilters: Ref<Record<string, string | string[] | undefined>>
139
+ filterMenus: Ref<Record<string, boolean>>
140
+ filtersOptions: Record<string, FilterOption[]>
141
+ meta: Ref<{ currentPage: number; perPage: number }>
142
+ enableMultiSelect?: boolean
143
+ selectedRows: any[]
144
+ }>()
145
+ const emit = defineEmits<TableEmits>()
178
146
 
147
+ const localSearches = ref<Record<string, string>>({})
148
+ const selected = ref<any[]>([])
149
+
150
+ const filteredOptions = computed(() => {
151
+ const result: Record<string, FilterOption[]> = {}
152
+ for (const col of props.columns) {
153
+ const search = localSearches.value[col.name]?.toLowerCase() || ''
154
+ const options = props.filtersOptions[col.name] || []
155
+ result[col.name] = options.filter(opt => opt.name.toLowerCase().includes(search))
156
+ }
157
+ return result
158
+ })
159
+
160
+ watch(
161
+ () => props.selectedRows,
162
+ val => {
163
+ selected.value = val
164
+ },
165
+ { immediate: true },
166
+ )
167
+
168
+ watch(
169
+ selected,
170
+ val => {
171
+ emit('update:selectedRows', val)
172
+ },
173
+ { deep: true },
174
+ )
175
+ </script>
179
176
  <style scoped lang="scss">
180
177
  .cursor-pointer {
181
178
  cursor: pointer;
@@ -1,27 +1,3 @@
1
- <script setup lang="ts">
2
- import AppTable from './AppTable.vue'
3
- import AppTablePagination from '../app-table/components/TablePagination.vue'
4
- import AppTableSearch from '../app-table/components/TableSearch.vue'
5
- import { defineProps, defineEmits } from 'vue'
6
-
7
- const emit = defineEmits<{
8
- 'update:selectedRows': [rows: any[]]
9
- }>()
10
- const props = defineProps<{
11
- search: string
12
- loading: boolean
13
- currentPage: number
14
- totalPages: number
15
- tableProps: any
16
- tableEvents: any
17
- onSearch: (val: string) => void
18
- onPageChange: (page: number) => void
19
- actionsSlot?: boolean
20
- modalSlot?: boolean
21
- selectedRows: any[]
22
- }>()
23
- </script>
24
-
25
1
  <template>
26
2
  <div class="table-layout">
27
3
  <div class="table-controls">
@@ -55,7 +31,29 @@ const props = defineProps<{
55
31
  <slot v-if="modalSlot" name="modal" />
56
32
  </div>
57
33
  </template>
34
+ <script setup lang="ts">
35
+ import AppTable from './AppTable.vue'
36
+ import AppTablePagination from '../app-table/components/TablePagination.vue'
37
+ import AppTableSearch from '../app-table/components/TableSearch.vue'
38
+ import { defineProps, defineEmits } from 'vue'
58
39
 
40
+ const props = defineProps<{
41
+ search: string
42
+ loading: boolean
43
+ currentPage: number
44
+ totalPages: number
45
+ tableProps: any
46
+ tableEvents: any
47
+ onSearch: (val: string) => void
48
+ onPageChange: (page: number) => void
49
+ actionsSlot?: boolean
50
+ modalSlot?: boolean
51
+ selectedRows: any[]
52
+ }>()
53
+ const emit = defineEmits<{
54
+ 'update:selectedRows': [rows: any[]]
55
+ }>()
56
+ </script>
59
57
  <style scoped lang="scss">
60
58
  .table-layout {
61
59
  height: calc(100vh - 100px);
@@ -1,154 +1,3 @@
1
- <script setup lang="ts">
2
- import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
3
- import AppModalSelect from '../components/ModalSelect.vue'
4
- import { useQuasar } from 'quasar'
5
- import { notificationSettings } from '@/utils/notification'
6
-
7
- const $q = useQuasar()
8
-
9
- type ModalMode = 'view' | 'edit' | 'create'
10
- type FieldType = 'text' | 'select'
11
-
12
- interface FieldOption {
13
- label: string
14
- value: string
15
- }
16
-
17
- interface FieldSchema {
18
- key: string
19
- label: string
20
- type: FieldType
21
- rules?: ((val: any) => boolean | string)[]
22
- options?: FieldOption[]
23
- placeholder?: string
24
- onSearch?: (val: string) => void
25
- onScroll?: () => void
26
- loading?: boolean
27
- }
28
-
29
- const props = defineProps<{
30
- modelValue: boolean
31
- title: string
32
- mode: ModalMode
33
- fields: FieldSchema[]
34
- initialData?: Record<string, any>
35
- }>()
36
-
37
- const emit = defineEmits<{
38
- (e: 'update:modelValue', val: boolean): void
39
- (e: 'submit', data: Record<string, any>): void
40
- (e: 'edit'): void
41
- (e: 'delete'): void
42
- }>()
43
-
44
- const formData = ref<Record<string, any>>({})
45
- const formRef = ref()
46
-
47
- watch(
48
- () => props.modelValue,
49
- val => {
50
- if (val) {
51
- formData.value = { ...(props.initialData ?? {}) }
52
- }
53
- },
54
- { immediate: true },
55
- )
56
- function isEqual(a: any, b: any): boolean {
57
- if (Array.isArray(a) && Array.isArray(b)) {
58
- if (a.length !== b.length) return false
59
- return a.every((item, i) => isEqual(item, b[i]))
60
- }
61
- if (typeof a === 'object' && typeof b === 'object') {
62
- return JSON.stringify(a) === JSON.stringify(b)
63
- }
64
- return a === b
65
- }
66
-
67
- function normalizeValue(val: any): any {
68
- if (Array.isArray(val)) {
69
- return val.map(v => (typeof v === 'object' && v !== null ? v.value ?? v : v))
70
- }
71
- return typeof val === 'object' && val !== null ? val.value ?? val : val
72
- }
73
-
74
- function submit() {
75
- formRef.value?.validate().then(ok => {
76
- if (!ok) return
77
-
78
- const changed: Record<string, any> = {}
79
-
80
- for (const field of props.fields) {
81
- const key = field.key
82
- const current = formData.value[key]
83
- const initial = props.initialData?.[key]
84
-
85
- const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
86
- const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
87
-
88
- if (!isEqual(normalizedCurrent, normalizedInitial)) {
89
- changed[key] = normalizedCurrent
90
- }
91
- }
92
-
93
- emit('submit', changed)
94
- })
95
- }
96
-
97
- function handleClear(key: string) {
98
- const field = props.fields.find(f => f.key === key)
99
- formData.value[key] = field?.type === 'select' ? [] : ''
100
-
101
- nextTick(() => {
102
- formRef.value?.validate()
103
- })
104
- }
105
- const isSubmitDisabled = computed(() => {
106
- if (props.mode === 'view') return false
107
-
108
- const hasEmptyRequired = props.fields.some(field => {
109
- const isRequired = field.rules?.some(rule => rule('') !== true)
110
- const val = formData.value[field.key]
111
- const normalized = normalizeValue(val)
112
-
113
- const empty =
114
- normalized === null ||
115
- normalized === undefined ||
116
- (typeof normalized === 'string' && normalized.trim() === '') ||
117
- (Array.isArray(normalized) && normalized.length === 0)
118
-
119
- return isRequired && empty
120
- })
121
-
122
- if (hasEmptyRequired) return true
123
-
124
- const hasChanges = props.fields.some(field => {
125
- const key = field.key
126
- const current = normalizeValue(formData.value[key])
127
- const initial = normalizeValue(props.initialData?.[key])
128
- return !isEqual(current, initial)
129
- })
130
-
131
- return !hasChanges
132
- })
133
- function uuidv4() {
134
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
135
- const r = (Math.random() * 16) | 0
136
- const v = c === 'x' ? r : (r & 0x3) | 0x8
137
- return v.toString(16)
138
- })
139
- }
140
-
141
- function generateUuid() {
142
- formData.value.uuid = uuidv4()
143
- }
144
- function copyToClipboard(text: string) {
145
- navigator.clipboard.writeText(text).then(() => {
146
- $q.notify(notificationSettings('success', 'UUID скопирован'))
147
- })
148
- }
149
- const filteredOptions = ref<Record<string, FieldOption[]>>({})
150
- </script>
151
-
152
1
  <template>
153
2
  <q-dialog :model-value="modelValue" @update:model-value="val => emit('update:modelValue', val)">
154
3
  <q-card style="min-width: 700px">
@@ -243,6 +92,129 @@ const filteredOptions = ref<Record<string, FieldOption[]>>({})
243
92
  </q-card>
244
93
  </q-dialog>
245
94
  </template>
95
+
96
+ <script setup lang="ts">
97
+ import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
98
+ import { useQuasar } from 'quasar'
99
+ import AppModalSelect from '../components/ModalSelect.vue'
100
+ import { notificationSettings } from '@/utils/notification'
101
+ import { isEqual, normalizeValue, uuidv4 } from '@/utils/helpers'
102
+
103
+ type ModalMode = 'view' | 'edit' | 'create'
104
+ type FieldType = 'text' | 'select'
105
+
106
+ interface FieldOption {
107
+ label: string
108
+ value: string
109
+ }
110
+ interface FieldSchema {
111
+ key: string
112
+ label: string
113
+ type: FieldType
114
+ rules?: ((val: any) => boolean | string)[]
115
+ options?: FieldOption[]
116
+ placeholder?: string
117
+ onSearch?: (val: string) => void
118
+ onScroll?: () => void
119
+ loading?: boolean
120
+ }
121
+
122
+ const props = defineProps<{
123
+ modelValue: boolean
124
+ title: string
125
+ mode: ModalMode
126
+ fields: FieldSchema[]
127
+ initialData?: Record<string, any>
128
+ }>()
129
+ const emit = defineEmits<{
130
+ (e: 'update:modelValue', val: boolean): void
131
+ (e: 'submit', data: Record<string, any>): void
132
+ (e: 'edit'): void
133
+ (e: 'delete'): void
134
+ }>()
135
+
136
+ const $q = useQuasar()
137
+ const formRef = ref()
138
+ const formData = ref<Record<string, any>>({})
139
+ const filteredOptions = ref<Record<string, FieldOption[]>>({})
140
+ const isSubmitDisabled = computed(() => {
141
+ if (props.mode === 'view') return false
142
+
143
+ const hasEmptyRequired = props.fields.some(field => {
144
+ const isRequired = field.rules?.some(rule => rule('') !== true)
145
+ const val = formData.value[field.key]
146
+ const normalized = normalizeValue(val)
147
+
148
+ const empty =
149
+ normalized === null ||
150
+ normalized === undefined ||
151
+ (typeof normalized === 'string' && normalized.trim() === '') ||
152
+ (Array.isArray(normalized) && normalized.length === 0)
153
+
154
+ return isRequired && empty
155
+ })
156
+
157
+ if (hasEmptyRequired) return true
158
+
159
+ const hasChanges = props.fields.some(field => {
160
+ const key = field.key
161
+ const current = normalizeValue(formData.value[key])
162
+ const initial = normalizeValue(props.initialData?.[key])
163
+ return !isEqual(current, initial)
164
+ })
165
+
166
+ return !hasChanges
167
+ })
168
+ function submit() {
169
+ formRef.value?.validate().then(ok => {
170
+ if (!ok) return
171
+
172
+ const changed: Record<string, any> = {}
173
+
174
+ for (const field of props.fields) {
175
+ const key = field.key
176
+ const current = formData.value[key]
177
+ const initial = props.initialData?.[key]
178
+
179
+ const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
180
+ const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
181
+
182
+ if (!isEqual(normalizedCurrent, normalizedInitial)) {
183
+ changed[key] = normalizedCurrent
184
+ }
185
+ }
186
+
187
+ emit('submit', changed)
188
+ })
189
+ }
190
+ function handleClear(key: string) {
191
+ const field = props.fields.find(f => f.key === key)
192
+ formData.value[key] = field?.type === 'select' ? [] : ''
193
+
194
+ nextTick(() => {
195
+ formRef.value?.validate()
196
+ })
197
+ }
198
+ function generateUuid() {
199
+ formData.value.uuid = uuidv4()
200
+ }
201
+ function copyToClipboard(text: string) {
202
+ navigator.clipboard.writeText(text).then(() => {
203
+ $q.notify(notificationSettings('success', 'UUID скопирован'))
204
+ })
205
+ }
206
+
207
+ watch(
208
+ () => props.modelValue,
209
+ val => {
210
+ if (val) {
211
+ formData.value = { ...(props.initialData ?? {}) }
212
+ }
213
+ },
214
+ { immediate: true },
215
+ )
216
+ </script>
217
+
246
218
  <style lang="scss">
247
219
  .custom-select-menu {
248
220
  max-height: 250px !important;
@@ -1,3 +1,27 @@
1
+ <template>
2
+ <div class="table-pagination">
3
+ <button class="arrow-button" :disabled="modelValue <= 1" @click="prevPage">
4
+ <q-icon name="mdi-chevron-left" :color="modelValue <= 1 ? 'grey-4' : 'primary'" />
5
+ </button>
6
+
7
+ <div class="pages">
8
+ <button
9
+ v-for="page in pageArray"
10
+ :key="page"
11
+ class="page-button"
12
+ :class="{ selected: page === modelValue, ellipsis: page === '...' }"
13
+ :disabled="page === modelValue || page === '...'"
14
+ @click="changePage(page)"
15
+ >
16
+ {{ page }}
17
+ </button>
18
+ </div>
19
+
20
+ <button class="arrow-button" :disabled="modelValue >= totalPages" @click="nextPage">
21
+ <q-icon name="mdi-chevron-right" :color="modelValue >= totalPages ? 'grey-4' : 'primary'" />
22
+ </button>
23
+ </div>
24
+ </template>
1
25
  <script setup lang="ts">
2
26
  import { computed } from 'vue'
3
27
  import { defineProps, defineEmits } from 'vue'
@@ -70,31 +94,6 @@ function nextPage() {
70
94
  }
71
95
  </script>
72
96
 
73
- <template>
74
- <div class="table-pagination">
75
- <button class="arrow-button" :disabled="modelValue <= 1" @click="prevPage">
76
- <q-icon name="mdi-chevron-left" :color="modelValue <= 1 ? 'grey-4' : 'primary'" />
77
- </button>
78
-
79
- <div class="pages">
80
- <button
81
- v-for="page in pageArray"
82
- :key="page"
83
- class="page-button"
84
- :class="{ selected: page === modelValue, ellipsis: page === '...' }"
85
- :disabled="page === modelValue || page === '...'"
86
- @click="changePage(page)"
87
- >
88
- {{ page }}
89
- </button>
90
- </div>
91
-
92
- <button class="arrow-button" :disabled="modelValue >= totalPages" @click="nextPage">
93
- <q-icon name="mdi-chevron-right" :color="modelValue >= totalPages ? 'grey-4' : 'primary'" />
94
- </button>
95
- </div>
96
- </template>
97
-
98
97
  <style scoped lang="scss">
99
98
  .pages {
100
99
  display: flex;
@@ -1,3 +1,11 @@
1
+ <template>
2
+ <q-input v-model="input" :placeholder="placeholder" dense outlined>
3
+ <template v-if="input" #append>
4
+ <q-icon name="✕" class="cursor-pointer" @click.stop="clearInput" />
5
+ </template>
6
+ </q-input>
7
+ </template>
8
+
1
9
  <script setup lang="ts">
2
10
  import { defineProps, defineEmits, ref, watch } from 'vue'
3
11
 
@@ -13,7 +21,10 @@ const emit = defineEmits<{
13
21
 
14
22
  const input = ref(props.modelValue)
15
23
  let debounceTimer: ReturnType<typeof setTimeout>
16
-
24
+ const clearInput = () => {
25
+ input.value = ''
26
+ emit('search', '')
27
+ }
17
28
  watch(
18
29
  () => props.modelValue,
19
30
  val => {
@@ -28,21 +39,8 @@ watch(input, val => {
28
39
  emit('search', val)
29
40
  }, 400)
30
41
  })
31
-
32
- const clearInput = () => {
33
- input.value = ''
34
- emit('search', '')
35
- }
36
42
  </script>
37
43
 
38
- <template>
39
- <q-input v-model="input" :placeholder="placeholder" dense outlined>
40
- <template v-if="input" #append>
41
- <q-icon name="✕" class="cursor-pointer" @click.stop="clearInput" />
42
- </template>
43
- </q-input>
44
- </template>
45
-
46
44
  <style scoped lang="scss">
47
45
  .q-field {
48
46
  background: #fff;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Сравнивает два значения на глубокое равенство.
3
+ * Поддерживает массивы, объекты и примитивы.
4
+ */
5
+ export function isEqual(a: any, b: any): boolean {
6
+ if (Array.isArray(a) && Array.isArray(b)) {
7
+ if (a.length !== b.length) return false
8
+ return a.every((item, i) => isEqual(item, b[i]))
9
+ }
10
+ if (typeof a === 'object' && typeof b === 'object') {
11
+ return JSON.stringify(a) === JSON.stringify(b)
12
+ }
13
+ return a === b
14
+ }
15
+
16
+ /**
17
+ * Нормализует значение:
18
+ * - Если передан массив объектов, возвращает массив `.value` или сам объект.
19
+ * - Если передан объект, возвращает `.value` или сам объект.
20
+ * - Если примитив — возвращает без изменений.
21
+ */
22
+ export function normalizeValue(val: any): any {
23
+ if (Array.isArray(val)) {
24
+ return val.map(v => (typeof v === 'object' && v !== null ? v.value ?? v : v))
25
+ }
26
+ return typeof val === 'object' && val !== null ? val.value ?? val : val
27
+ }
28
+
29
+ /**
30
+ * Генерирует UUID v4.
31
+ * Используется для идентификаторов временных сущностей.
32
+ */
33
+ export function uuidv4(): string {
34
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
35
+ const r = (Math.random() * 16) | 0
36
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
37
+ return v.toString(16)
38
+ })
39
+ }