@tak-ps/vue-tabler 4.1.1 → 4.2.0

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/CHANGELOG.md CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  ## Version History
12
12
 
13
+ ### v4.2.0
14
+
15
+ - :tada: Add JSON Schema Component
16
+
13
17
  ### v4.1.1
14
18
 
15
19
  - :bug: Add missing component
@@ -24,9 +24,10 @@
24
24
  "mb-4 mt-2": !compact
25
25
  }'
26
26
  >
27
- <div class='user-select-none'>
28
- <span v-text='label' />
29
- </div>
27
+ <div
28
+ class='user-select-none'
29
+ v-text='label'
30
+ >
30
31
  </div>
31
32
 
32
33
  <div
@@ -0,0 +1,297 @@
1
+ <template>
2
+ <div class='card'>
3
+ <div class='card-header'>
4
+ <h3
5
+ class='card-title'
6
+ v-text='title'
7
+ />
8
+
9
+ <div class='ms-auto btn-list'>
10
+ <div class='dropdown'>
11
+ <div
12
+ id='dropdownMenuButton1'
13
+ class='dropdown-toggle'
14
+ type='button'
15
+ data-bs-toggle='dropdown'
16
+ aria-expanded='false'
17
+ >
18
+ <IconPlus
19
+ :size='32'
20
+ stroke='1'
21
+ class='cursor-pointer'
22
+ />
23
+ </div>
24
+ <ul
25
+ class='dropdown-menu'
26
+ aria-labelledby='dropdownMenuButton1'
27
+ >
28
+ <div class='m-1 text-center'>
29
+ <div
30
+ class='opt cursor-pointer py-1'
31
+ @click='create({
32
+ "name": "",
33
+ "type": "string",
34
+ "required": false,
35
+ })'
36
+ >
37
+ String
38
+ </div>
39
+ <div
40
+ class='opt cursor-pointer py-1'
41
+ @click='create({
42
+ "name": "",
43
+ "type": "string",
44
+ "enum": [],
45
+ "required": false,
46
+ })'
47
+ >
48
+ Enum
49
+ </div>
50
+ <div
51
+ class='opt cursor-pointer py-1'
52
+ @click='create({
53
+ "name": "",
54
+ "type": "boolean",
55
+ "required": false,
56
+ })'
57
+ >
58
+ Boolean
59
+ </div>
60
+ <div
61
+ class='opt cursor-pointer py-1'
62
+ @click='create({
63
+ "name": "",
64
+ "type": "number",
65
+ "required": false,
66
+ })'
67
+ >
68
+ Number
69
+ </div>
70
+ <div
71
+ class='opt cursor-pointer py-1'
72
+ @click='create({
73
+ "name": "",
74
+ "type": "integer",
75
+ "required": false,
76
+ })'
77
+ >
78
+ Integer
79
+ </div>
80
+ </div>
81
+ </ul>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ <div class='card-body'>
86
+ <TablerNone
87
+ v-if='!schema.length'
88
+ label='No Properties'
89
+ :create='false'
90
+ :compact='true'
91
+ />
92
+ <template v-else>
93
+ <div
94
+ v-for='(prop, i) in schema'
95
+ :key='prop._id'
96
+ class='col-12 hover-edit cursor-pointer relative'
97
+ @click='editIdx = i; modal = prop'
98
+ >
99
+ <div class='delete-btn'>
100
+ <TablerDelete
101
+ displaytype='icon'
102
+ :size='24'
103
+ @delete='remove(i)'
104
+ />
105
+ </div>
106
+ <div class='overlay' />
107
+ <TablerEnum
108
+ v-if='prop.enum'
109
+ :model-value='input[prop.name]'
110
+ :label='prop.name'
111
+ :disabled='true'
112
+ :required='prop.required || false'
113
+ :description='prop.description || ""'
114
+ :options='prop.enum'
115
+ />
116
+ <TablerToggle
117
+ v-else-if='prop.type === "boolean"'
118
+ :model-value='input[prop.name]'
119
+ :label='prop.name'
120
+ :disabled='true'
121
+ :required='prop.required || false'
122
+ :description='prop.description || ""'
123
+ />
124
+ <TablerInput
125
+ v-else
126
+ :model-value='input[prop.name]'
127
+ :label='prop.name'
128
+ :disabled='true'
129
+ :required='prop.required || false'
130
+ :description='prop.description || ""'
131
+ />
132
+ </div>
133
+ </template>
134
+ </div>
135
+
136
+ <BuilderEdit
137
+ v-if='modal'
138
+ :prop='modal'
139
+ @close='modal = null'
140
+ @done='save($event)'
141
+ />
142
+ </div>
143
+ </template>
144
+
145
+ <script setup lang="ts">
146
+ import { ref, computed, onMounted, watch } from 'vue';
147
+ import {
148
+ IconPlus,
149
+ } from '@tabler/icons-vue';
150
+ import BuilderEdit from './SchemaBuilderEdit.vue';
151
+ import {
152
+ TablerNone,
153
+ TablerInput,
154
+ TablerEnum,
155
+ TablerToggle,
156
+ TablerDelete
157
+ } from '@tak-ps/vue-tabler';
158
+
159
+ interface SchemaProperty {
160
+ name: string;
161
+ type: string;
162
+ required?: boolean;
163
+ description?: string;
164
+ enum?: string[];
165
+ _id?: string;
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ [key: string]: any;
168
+ }
169
+
170
+ interface JSONSchema {
171
+ type: string;
172
+ required: string[];
173
+ additionalProperties: boolean;
174
+ properties: Record<string, SchemaProperty>;
175
+ }
176
+
177
+ const props = withDefaults(defineProps<{
178
+ title?: string;
179
+ modelValue: {
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ properties: Record<string, any>;
182
+ required?: string[];
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ [key: string]: any;
185
+ };
186
+ }>(), {
187
+ title: 'JSON Schema Builder'
188
+ });
189
+
190
+ const emit = defineEmits<{
191
+ (e: 'update:modelValue', value: JSONSchema): void;
192
+ }>();
193
+
194
+ const modal = ref<SchemaProperty | null>(null);
195
+ const editIdx = ref<number | null>(null);
196
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
+ const input = ref<Record<string, any>>({});
198
+ const schema = ref<SchemaProperty[]>([]);
199
+
200
+ const computedSchema = computed<JSONSchema>(() => {
201
+ const res: JSONSchema = {
202
+ type: 'object',
203
+ required: [],
204
+ additionalProperties: false,
205
+ properties: {}
206
+ }
207
+
208
+ for (const prop of JSON.parse(JSON.stringify(schema.value))) {
209
+ const name = prop.name;
210
+ delete prop.name;
211
+ delete prop._id;
212
+
213
+ if (prop.required) res.required.push(name);
214
+ delete prop.required;
215
+ res.properties[name] = prop;
216
+ }
217
+
218
+ return res;
219
+ });
220
+
221
+ watch(computedSchema, (val) => {
222
+ emit('update:modelValue', val);
223
+ });
224
+
225
+ onMounted(() => {
226
+ for (const prop in props.modelValue.properties) {
227
+ schema.value.push({
228
+ name: prop,
229
+ required: (props.modelValue.required || []).includes(prop),
230
+ ...props.modelValue.properties[prop],
231
+ _id: Math.random().toString(36).substring(7)
232
+ });
233
+ }
234
+ });
235
+
236
+ function create(prop: SchemaProperty) {
237
+ prop._id = Math.random().toString(36).substring(7);
238
+ editIdx.value = null;
239
+ modal.value = prop;
240
+ }
241
+
242
+ function save(prop: SchemaProperty) {
243
+ if (editIdx.value !== null) {
244
+ schema.value[editIdx.value] = prop;
245
+ } else {
246
+ schema.value.push(prop);
247
+ }
248
+ modal.value = null;
249
+ editIdx.value = null;
250
+ }
251
+
252
+ function remove(i: number) {
253
+ schema.value.splice(i, 1);
254
+ }
255
+ </script>
256
+
257
+ <style>
258
+ .opt:hover {
259
+ font-weight: 900;
260
+ }
261
+
262
+ .hover-edit {
263
+ position: relative;
264
+ border: 1px solid transparent;
265
+ border-radius: 4px;
266
+ transition: all 0.2s;
267
+ padding: 12px;
268
+ }
269
+
270
+ .hover-edit:hover {
271
+ background-color: rgba(0, 0, 0, 0.02);
272
+ border-color: #f0f0f0;
273
+ }
274
+
275
+ .hover-edit .delete-btn {
276
+ opacity: 0;
277
+ transition: opacity 0.2s;
278
+ position: absolute;
279
+ top: 5px;
280
+ right: 5px;
281
+ z-index: 20;
282
+ }
283
+
284
+ .hover-edit:hover .delete-btn {
285
+ opacity: 1;
286
+ }
287
+
288
+ .hover-edit .overlay {
289
+ position: absolute;
290
+ top: 0;
291
+ left: 0;
292
+ width: 100%;
293
+ height: 100%;
294
+ z-index: 10;
295
+ cursor: pointer;
296
+ }
297
+ </style>
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <TablerModal>
3
+ <button
4
+ type='button'
5
+ class='btn-close'
6
+ aria-label='Close'
7
+ @click='$emit("close")'
8
+ />
9
+ <div class='modal-status bg-yellow' />
10
+ <div class='modal-header'>
11
+ <div class='d-flex align-items-center my-2'>
12
+ <template v-if='edit.type === "string"'>
13
+ <IconAlphabetLatin
14
+ :size='32'
15
+ stroke='1'
16
+ />
17
+ </template>
18
+ <template v-else-if='edit.type === "number"'>
19
+ <DecimalIcon
20
+ :size='32'
21
+ stroke='1'
22
+ />
23
+ </template>
24
+ <template v-else-if='edit.type === "integer"'>
25
+ <Sort09Icon
26
+ :size='32'
27
+ stroke='1'
28
+ />
29
+ </template>
30
+ <template v-else>
31
+ <BinaryIcon
32
+ :size='32'
33
+ stroke='1'
34
+ />
35
+ </template>
36
+ <span
37
+ class='my-1 mx-2 strong'
38
+ v-text='edit.type'
39
+ />
40
+ </div>
41
+ </div>
42
+ <div class='modal-body text-center py-4'>
43
+ <div class='row g-2 d-flex'>
44
+ <TablerInput
45
+ v-model='edit.name'
46
+ label='Field Name'
47
+ :required='true'
48
+ />
49
+ <TablerToggle
50
+ v-model='edit.required'
51
+ label='Required'
52
+ />
53
+ <TablerInput
54
+ v-model='edit.description'
55
+ :rows='3'
56
+ label='Description'
57
+ />
58
+
59
+ <template v-if='edit.type === "string" && edit.enum === undefined' />
60
+ <template v-else-if='edit.type === "string" && Array.isArray(edit.enum)'>
61
+ <div class='subheader mt-2'>
62
+ Enum Values
63
+ </div>
64
+ <div class='col-12 d-flex'>
65
+ <TablerInput
66
+ v-model='newEnum'
67
+ class='flex-grow-1'
68
+ placeholder='New Value'
69
+ @keyup.enter='addEnum'
70
+ />
71
+ <button
72
+ class='btn btn-icon ms-2'
73
+ @click='addEnum'
74
+ >
75
+ <IconPlus
76
+ :size='20'
77
+ stroke='1'
78
+ />
79
+ </button>
80
+ </div>
81
+ <div class='list-group list-group-flush mt-2'>
82
+ <div
83
+ v-for='(val, i) in edit.enum'
84
+ :key='i'
85
+ class='list-group-item d-flex justify-content-between align-items-center py-1'
86
+ >
87
+ <span v-text='val' />
88
+ <IconTrash
89
+ :size='16'
90
+ stroke='1'
91
+ class='cursor-pointer text-danger'
92
+ @click='edit.enum?.splice(i, 1)'
93
+ />
94
+ </div>
95
+ </div>
96
+ </template>
97
+ <template v-else-if='edit.type === "number"' />
98
+ <template v-else-if='edit.type === "integer"' />
99
+ </div>
100
+ </div>
101
+ <div class='modal-footer'>
102
+ <button
103
+ class='btn btn-primary'
104
+ :disabled='!edit.name.length'
105
+ @click='$emit("done", edit)'
106
+ >
107
+ Save
108
+ </button>
109
+ </div>
110
+ </TablerModal>
111
+ </template>
112
+
113
+ <script setup lang="ts">
114
+ import { ref } from 'vue';
115
+ import {
116
+ IconAlphabetLatin,
117
+ IconPlus,
118
+ IconTrash
119
+ } from '@tabler/icons-vue';
120
+ import {
121
+ TablerModal,
122
+ TablerInput,
123
+ TablerToggle
124
+ } from '@tak-ps/vue-tabler';
125
+
126
+ interface SchemaProperty {
127
+ name: string;
128
+ type: string;
129
+ required?: boolean;
130
+ description?: string;
131
+ enum?: string[];
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ [key: string]: any;
134
+ }
135
+
136
+ const props = defineProps<{
137
+ prop: SchemaProperty
138
+ }>();
139
+
140
+ defineEmits<{
141
+ (e: 'close'): void;
142
+ (e: 'done', value: SchemaProperty): void;
143
+ }>();
144
+
145
+ const edit = ref<SchemaProperty>(JSON.parse(JSON.stringify(props.prop)));
146
+ const newEnum = ref('');
147
+
148
+ function addEnum() {
149
+ if (!newEnum.value.trim()) return;
150
+ if (!edit.value.enum) edit.value.enum = [];
151
+ edit.value.enum.push(newEnum.value.trim());
152
+ newEnum.value = '';
153
+ }
154
+ </script>
package/lib.ts CHANGED
@@ -30,3 +30,4 @@ export { default as TablerEpochRange } from './components/EpochRange.vue';
30
30
  export { default as TablerMarkdown } from './components/Markdown.vue';
31
31
  export { default as TablerDelete } from './components/Delete.vue';
32
32
  export { default as TablerSchema } from './components/Schema.vue';
33
+ export { default as TablerSchemaBuilder } from './components/SchemaBuilder.vue';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tak-ps/vue-tabler",
3
3
  "type": "module",
4
- "version": "4.1.1",
4
+ "version": "4.2.0",
5
5
  "lib": "lib.ts",
6
6
  "main": "lib.ts",
7
7
  "module": "lib.ts",