flameresttable 1.0.28 → 1.0.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flameresttable",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "main": "./src/plugin.ts",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,15 +23,17 @@
23
23
  "pug-lexer": "github:FlameArt/pug-lexer-only",
24
24
  "sass": "^1.69.5",
25
25
  "vue": "^3.3.7",
26
- "vue-router": "^4.2.5"
26
+ "vue-router": "^4.2.5",
27
+ "vue-select": "^4.0.0-beta.6"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@heroicons/vue": "^2.0.18",
30
31
  "@types/lodash-es": "^4.17.10",
31
32
  "@types/lodash.merge": "^4.6.8",
32
33
  "@types/node": "20.8.9",
33
- "@typescript-eslint/eslint-plugin": "^6.9.0",
34
- "@typescript-eslint/parser": "^6.9.0",
34
+ "@types/vue-select": "^3.16.6",
35
+ "@typescript-eslint/eslint-plugin": "^6.9.1",
36
+ "@typescript-eslint/parser": "^6.9.1",
35
37
  "@vitejs/plugin-vue": "^4.4.0",
36
38
  "@volar/vue-language-plugin-pug": "^1.6.5",
37
39
  "@vue/compiler-sfc": "^3.3.7",
package/src/App.vue CHANGED
@@ -4,6 +4,7 @@ import Table from './Table/Table.vue';
4
4
  import { ref } from 'vue';
5
5
  import Model from '@models/Testtable';
6
6
  import TableOpts from './Table/TableOpts';
7
+ import Relatedtable from '@models/Relatedtable';
7
8
 
8
9
  const store = storeFile(), router = useRouter(), route = useRoute();
9
10
 
@@ -24,7 +25,7 @@ const opts = new TableOpts;
24
25
  opts.set('name', { title: 'Название' });
25
26
  opts.set('dt1', { title: 'Дата 1' });
26
27
  opts.set('dt2', { title: 'Дата 2' });
27
- opts.set('id', { title: 'Дата 2', Filter: { isShow: false } });
28
+ opts.set('id', { title: 'ID', Selector: { model: Relatedtable }, Filter: { isShow: true, type: 'selector', selector: { multiselect: true, mode: 'vertical' } } });
28
29
  opts.set('file', { title: 'файл', Filter: { isShow: false } });
29
30
  opts.set("name", {
30
31
  title: 'Название', Table:
@@ -1,3 +1,5 @@
1
+ import ITableSelectorItem from "./TableSelectorItem";
2
+
1
3
  // Подгрузчик типа класса
2
4
  type Class<T> = new (...args: any[]) => T
3
5
 
@@ -86,24 +88,30 @@ export class Column implements IColumn {
86
88
  */
87
89
  public Selector = {
88
90
 
91
+ /**
92
+ * Нужна авто-предзагрузка значений при загрузке страницы
93
+ * [полная таблица]
94
+ */
95
+ preload: true,
96
+
89
97
  /**
90
98
  * Загрузчик значений
91
- * @returns Array
99
+ * @returns ITableSelectorItem[]
92
100
  */
93
- loader: () => [],
101
+ loader: null as ((rows?: Array<any>) => ITableSelectorItem[]) | null,
94
102
 
95
103
  /**
96
104
  * Модель, с которой выгружать значения
97
105
  */
98
- model: null as Class<any> | null,
106
+ model: null as any | null,
99
107
 
100
108
  /**
101
109
  * Здесь список готовых значений
102
110
  */
103
- values: [],
111
+ values: [] as ITableSelectorItem[],
104
112
 
105
113
  /**
106
- * Здесь можно определить кастомный лоадер при поиске
114
+ * Кастомизация вывода отдельного элемента
107
115
  * @param row
108
116
  * @param col
109
117
  * @returns
@@ -130,7 +138,10 @@ export class Column implements IColumn {
130
138
  /**
131
139
  * Список значений под выбор
132
140
  */
133
- select: [],
141
+ selector: {
142
+ mode: 'vertical' as 'vertical' | 'horizontal',
143
+ multiselect: true,
144
+ },
134
145
 
135
146
  // Минимальное число символов в строке фильтра, после которого будет произведён запрос
136
147
  filterMinSymbolsRequest: 0,
@@ -305,6 +316,14 @@ export interface IColumn {
305
316
  filterRow?: number,
306
317
  filterColumn?: number,
307
318
 
319
+ /**
320
+ * Список значений под выбор
321
+ */
322
+ selector?: {
323
+ mode?: 'vertical' | 'horizontal',
324
+ multiselect?: boolean,
325
+ },
326
+
308
327
  /**
309
328
  * CSS-классы, применяемые к фильтру,
310
329
  */
@@ -312,6 +331,43 @@ export interface IColumn {
312
331
 
313
332
  },
314
333
 
334
+ /**
335
+ * Это единые related данные для всех селекторов:
336
+ * внутри таблицы для замены id названий на значения
337
+ * селектор для фильтров
338
+ * селектор в попапе
339
+ */
340
+ Selector?: {
341
+
342
+ /**
343
+ * Загрузчик значений ручной
344
+ * @returns ITableSelectorItem[]
345
+ */
346
+ loader?: ((rows?: Array<any>) => ITableSelectorItem[]) | null,
347
+
348
+ /**
349
+ * Здесь список готовых значений
350
+ */
351
+ values?: ITableSelectorItem[],
352
+
353
+
354
+ /**
355
+ * Модель, с которой выгружать значения
356
+ */
357
+ model?: Class<any> | null,
358
+
359
+
360
+ /**
361
+ * Кастомизация вывода отдельного элемента
362
+ * @param row
363
+ * @param col
364
+ * @returns
365
+ */
366
+ value?: (row: any, col: string) => string
367
+
368
+ },
369
+
370
+
315
371
  Popup?: {
316
372
 
317
373
  // Тут привязка к реальным данным
@@ -106,10 +106,11 @@ export default class FlameTable<T> {
106
106
  // TODO: Сортируем колонки, если указан порядок
107
107
  //this.columns
108
108
 
109
-
110
-
111
109
  // Далее юзер просто будет делать set("колонка", опции)
112
110
 
111
+ // Предзагружаем колонки
112
+ this.preloadRelated()
113
+
113
114
  }
114
115
 
115
116
  /**
@@ -135,6 +136,62 @@ export default class FlameTable<T> {
135
136
 
136
137
  }
137
138
 
139
+ public async preloadRelated() {
140
+
141
+ const allPreloadPromises: Promise<any>[] = [];
142
+ for (const key in this.columns) {
143
+ const col = this.columns[key];
144
+
145
+ // только где предзагрузка включена
146
+ if (col.Selector.preload !== true) continue;
147
+
148
+ // если указана модель, то загружаем через неё
149
+ if (col.Selector.model !== null)
150
+ allPreloadPromises.push(col.Selector.model.prototype.constructor.all({
151
+ perPage: 1000 // лимит в 1000 ответов для уменьшения дефолтного нагруза
152
+ }).then((r: any) => {
153
+
154
+ // Значение загрузилось: делаем преобразования
155
+
156
+ // ключ модели
157
+ const pk = (col.Selector.model).prototype.constructor['primaryKeys'][0];
158
+
159
+ // Ручные
160
+ if (typeof col.Selector.loader === 'function') {
161
+ col.Selector.values.push(...col.Selector.loader(r.data))
162
+ }
163
+
164
+ // Преобразования не найдено, попытка автоматически определить параметры
165
+ if (col.Selector.loader === null) {
166
+ const rows = r.data;
167
+ for (const row of rows) {
168
+ if (typeof row[pk] !== 'undefined') {
169
+
170
+ let name = typeof row['name'] !== 'undefined' ? row['name'].toString() : null;
171
+ name = name ?? (typeof row['title'] !== 'undefined' ? row['title'].toString() : null);
172
+ name = name ?? (typeof row['desc'] !== 'undefined' ? row['desc'].toString() : null);
173
+ const color = (typeof row['color'] !== 'undefined' ? row['color'].toString() : null);
174
+ const position = typeof row[pk] === 'number' ? row[pk] : 0;
175
+
176
+ col.Selector.values.push({
177
+ id: row[pk],
178
+ title: name,
179
+ label: name,
180
+ color: color,
181
+ position: position
182
+ })
183
+
184
+ }
185
+ }
186
+ }
187
+ }))
188
+ }
189
+
190
+ // Начинаем процесс предзагрузки
191
+ await Promise.allSettled(allPreloadPromises);
192
+
193
+ }
194
+
138
195
  /**
139
196
  * Обновить результаты от RESTа
140
197
  * @param SaveLoadParams Сохранить стандартные параметры загрузки
@@ -166,7 +223,6 @@ export default class FlameTable<T> {
166
223
  case 'date':
167
224
  case 'fixed':
168
225
  case 'fulltext':
169
- case 'selector':
170
226
  case 'text':
171
227
  customFilters.params[key] = el.valueString;
172
228
  continue;
@@ -174,6 +230,13 @@ export default class FlameTable<T> {
174
230
  case 'number':
175
231
  customFilters.params[key] = el.valueRange;
176
232
  continue;
233
+ case 'selector':
234
+ if (this.columns[key].Filter.selector.multiselect) {
235
+ customFilters.params[key] = el.valueRange;
236
+ }
237
+ else
238
+ customFilters.params[key] = el.valueString;
239
+ continue;
177
240
  default:
178
241
  alert('НЕИЗВЕСТНЫЙ ТИП')
179
242
  continue;
@@ -200,7 +263,6 @@ export default class FlameTable<T> {
200
263
  // Точное совпадение
201
264
  case 'fixed':
202
265
  case 'date':
203
- case 'selector':
204
266
 
205
267
  if (el.valueString.trim() === '') continue;
206
268
  customFilters.where[key] = el.valueString;
@@ -218,6 +280,17 @@ export default class FlameTable<T> {
218
280
 
219
281
  break;
220
282
 
283
+ case 'selector':
284
+
285
+ if (el.selector.multiselect) {
286
+ if (el.valueRange.length > 0)
287
+ customFilters.where[key] = ['IN', key, el.valueRange];
288
+ }
289
+ else
290
+ customFilters.where[key] = el.valueString;
291
+
292
+ break;
293
+
221
294
  default:
222
295
  alert('Unknown table filter type');
223
296
  }
@@ -247,6 +247,7 @@ type Class<T> = new (...args: any[]) => T;
247
247
  import Datepicker from '@vuepic/vue-datepicker';
248
248
  import '@vuepic/vue-datepicker/dist/main.css';
249
249
  import { isNumber } from 'lodash';
250
+ import 'vue-select/dist/vue-select.css';
250
251
 
251
252
 
252
253
  export default defineComponent({
@@ -1,22 +1,40 @@
1
1
  <template>
2
2
  <div class="fc flex-wrap">
3
- <div v-for="(col) in props.columns" :key="col.name" v-show="col.Filter.isShow" :class="col.Filter.classes">
3
+ <div v-for="(col) in props.columns" :key="col.name" v-show="col.Filter.isShow"
4
+ :class="col.Filter.classes + (col.Filter.type === 'selector' && col.Filter.selector.mode === 'horizontal' ? ' w-full ' : '')">
4
5
 
5
- <!-- ТЕКСТ -->
6
6
  <div class="fc flex-col">
7
7
  <div class="text-xs text-slate-400">{{ col.title.toUpperCase() ?? col.name }}</div>
8
+
9
+ <!-- ТЕКСТ -->
8
10
  <div v-if="col.Filter.type === 'text' || col.Filter.type === 'fixed' || col.Filter.type === 'fulltext'">
9
11
  <input class="outline-none border border-slate-500 px-2 py-1 mx-2" :placeholder="col.title ?? col.name"
10
12
  type="text" @keyup="update()" v-model="col.Filter.valueString" />
11
13
  </div>
14
+
15
+ <!-- СЕЛЕКТОРЫ -->
12
16
  <div v-if="col.Filter.type === 'selector'">
13
- <select class="outline-none border border-slate-500 px-2 py-1 mx-2" :placeholder="col.title ?? col.name"
14
- @change="update()" v-model="col.Filter.valueString">
15
- <option v-for="item in col.Selector.values" :key="item[0]" value="">
16
- {{ item[1] }}
17
+ <!-- ВЕРТИКАЛЬНЫЕ -->
18
+ <!--<select v-if="col.Filter.selector.mode === 'vertical'" :multiple="col.Filter.selector.multiselect"
19
+ class="outline-none border border-slate-500 px-2 py-1 mx-2" :placeholder="col.title ?? col.name"
20
+ @change="update(col)" v-model="col.Filter.valueString">
21
+ <option v-for="item in col.Selector.values" :key="item.id" :value="item.id">
22
+ {{ item.title }}
17
23
  </option>
18
- </select>
24
+ </select>-->
25
+ <v-select :multiple="col.Filter.selector.multiselect" v-model="col.Filter.valueString"
26
+ :options="col.Selector.values" class="min-w-[150px]" @option:selected="update(col)"
27
+ @option:deselected="update(col)" />
19
28
  </div>
29
+ <div v-if="col.Filter.type === 'selector' && col.Filter.selector.mode === 'horizontal'"
30
+ class="w-full text-center">
31
+ <!-- ГОРИЗОНТАЛЬНЫЕ -->
32
+ <div v-for="item in col.Selector.values" :key="item.id" class="px-2 py-1 mx-1 inline-block rounded-[30px]"
33
+ :style="'background-color:' + (item.color ?? 'rgb(148,163,184)')">
34
+ {{ item.title }}
35
+ </div>
36
+ </div>
37
+
20
38
  <div v-if="col.Filter.type === 'date' || col.Filter.type === 'daterange'">
21
39
  <Datepicker v-if="col.Filter.type === 'date'" v-model="col.Filter.valueString" :auto-apply="true"
22
40
  :enable-time-picker="false" @update:model-value="update()" :teleport="true"></Datepicker>
@@ -31,22 +49,24 @@
31
49
  </template>
32
50
 
33
51
  <script setup lang="ts" generic="T">
34
- import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import type { Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest"
52
+ import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import { computed, type Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest"
35
53
 
36
54
  // Иконки
37
55
  import { XCircleIcon } from '@icons/24/solid'
38
56
  import TableOpts from './TableOpts';
39
- import { Column } from './Columns';
57
+ import { Column, IColumn } from './Columns';
40
58
  //import { TableFilter } from './TableOpts.js';
41
59
  import FlameTable from './FlameTable';
42
60
  import Datepicker from '@vuepic/vue-datepicker';
61
+ import vSelect from 'vue-select'
62
+ import ITableSelectorItem from './TableSelectorItem';
43
63
 
44
64
  // Глобальное хранилище и роуты
45
65
  const store = storeFile(), router = useRouter(), route = useRoute();
46
66
 
47
67
  // Локальное состояние компонента
48
68
  const state = reactive({
49
- data: {}
69
+ data: {},
50
70
  })
51
71
 
52
72
  // Входящие данные компонента
@@ -61,12 +81,20 @@ onMounted(async () => {
61
81
  })
62
82
 
63
83
 
64
- const update = () => {
84
+ const update = (col?: Column) => {
85
+
86
+ // селектор преобразуем к v-model
87
+ if (col && col.Filter?.selector?.multiselect) {
88
+ col.Filter.valueRange = (col.Filter.valueString as any).map((r: ITableSelectorItem) => r.id);
89
+ }
65
90
 
66
91
  props.table.update();
67
92
 
68
93
  }
69
94
 
95
+
96
+
97
+
70
98
  </script>
71
99
 
72
100
  <style scoped lang="scss"></style>
@@ -0,0 +1,9 @@
1
+ export default interface ITableSelectorItem {
2
+
3
+ id: string | number,
4
+ title?: string,
5
+ label?: string,
6
+ color?: string,
7
+ position?: number
8
+
9
+ }
package/src/main.ts CHANGED
@@ -7,6 +7,7 @@ import router from "./router";
7
7
  import 'vue-universal-modal/dist/index.css'
8
8
  import VueUniversalModal from 'vue-universal-modal'
9
9
  import REST from 'flamerest';
10
+ import vSelect from 'vue-select'
10
11
 
11
12
 
12
13
  REST.SERVER = 'http://testrest';
@@ -15,5 +16,6 @@ REST.SERVER = 'http://testrest';
15
16
  createApp(App)
16
17
  .use(createPinia())
17
18
  .use(router)
19
+ .use(vSelect as any)
18
20
  .use(VueUniversalModal, { teleportTarget: '#my-modals', modalComponent: 'CustomModal' })
19
21
  .mount('#app')