adminforth 1.3.54-next.17 → 1.3.54-next.19

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.
@@ -117,7 +117,11 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
117
117
  return !!value;
118
118
  } else if (field.type == AdminForthDataTypes.JSON) {
119
119
  if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
120
- return JSON.parse(value);
120
+ try {
121
+ return JSON.parse(value);
122
+ } catch (e) {
123
+ return {'error': `Failed to parse JSON: ${e.message}`}
124
+ }
121
125
  } else {
122
126
  console.error(`AdminForth: JSON field is not a string but ${field._underlineType}, this is not supported yet`);
123
127
  }
@@ -153,7 +153,11 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
153
153
 
154
154
  if (field.type == AdminForthDataTypes.JSON) {
155
155
  if (typeof value == 'string') {
156
- return JSON.parse(value);
156
+ try {
157
+ return JSON.parse(value);
158
+ } catch (e) {
159
+ return {'error': `Failed to parse JSON: ${e.message}`}
160
+ }
157
161
  } else if (typeof value == 'object') {
158
162
  return value;
159
163
  } else {
@@ -87,7 +87,11 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
87
87
  return !!value;
88
88
  } else if (field.type == AdminForthDataTypes.JSON) {
89
89
  if (field._underlineType == 'text' || field._underlineType == 'varchar') {
90
- return JSON.parse(value);
90
+ try {
91
+ return JSON.parse(value);
92
+ } catch (e) {
93
+ return {'error': `Failed to parse JSON: ${e.message}`}
94
+ }
91
95
  } else {
92
96
  console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
93
97
  }
@@ -145,7 +145,12 @@ class ClickhouseConnector extends AdminForthBaseConnector {
145
145
  }
146
146
  else if (field.type == AdminForthDataTypes.JSON) {
147
147
  if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
148
- return JSON.parse(value);
148
+ try {
149
+ return JSON.parse(value);
150
+ }
151
+ catch (e) {
152
+ return { 'error': `Failed to parse JSON: ${e.message}` };
153
+ }
149
154
  }
150
155
  else {
151
156
  console.error(`AdminForth: JSON field is not a string but ${field._underlineType}, this is not supported yet`);
@@ -154,7 +154,12 @@ class PostgresConnector extends AdminForthBaseConnector {
154
154
  }
155
155
  if (field.type == AdminForthDataTypes.JSON) {
156
156
  if (typeof value == 'string') {
157
- return JSON.parse(value);
157
+ try {
158
+ return JSON.parse(value);
159
+ }
160
+ catch (e) {
161
+ return { 'error': `Failed to parse JSON: ${e.message}` };
162
+ }
158
163
  }
159
164
  else if (typeof value == 'object') {
160
165
  return value;
@@ -120,7 +120,12 @@ class SQLiteConnector extends AdminForthBaseConnector {
120
120
  }
121
121
  else if (field.type == AdminForthDataTypes.JSON) {
122
122
  if (field._underlineType == 'text' || field._underlineType == 'varchar') {
123
- return JSON.parse(value);
123
+ try {
124
+ return JSON.parse(value);
125
+ }
126
+ catch (e) {
127
+ return { 'error': `Failed to parse JSON: ${e.message}` };
128
+ }
124
129
  }
125
130
  else {
126
131
  console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
@@ -12,6 +12,7 @@ import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { guessLabelFromName, suggestIfTypo } from './utils.js';
14
14
  import crypto from 'crypto';
15
+ import { AdminForthSortDirections } from "adminforth/index.js";
15
16
  export default class ConfigValidator {
16
17
  constructor(adminforth, config) {
17
18
  this.adminforth = adminforth;
@@ -222,6 +223,22 @@ export default class ConfigValidator {
222
223
  all: true,
223
224
  };
224
225
  }
226
+ if (res.options.defaultSort) {
227
+ const colName = res.options.defaultSort.columnName;
228
+ const col = res.columns.find((c) => c.name === colName);
229
+ if (!col) {
230
+ const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
231
+ errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
232
+ }
233
+ const dir = res.options.defaultSort.direction;
234
+ if (!dir) {
235
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
236
+ }
237
+ // AdminForthSortDirections is enum
238
+ if (!Object.values(AdminForthSortDirections).includes(dir)) {
239
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
240
+ }
241
+ }
225
242
  if (Object.keys(res.options.allowedActions).includes('all')) {
226
243
  if (Object.keys(res.options.allowedActions).length > 1) {
227
244
  errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
@@ -14,6 +14,7 @@ import path from 'path';
14
14
  import { guessLabelFromName, suggestIfTypo } from './utils.js';
15
15
 
16
16
  import crypto from 'crypto';
17
+ import { AdminForthSortDirections } from "adminforth/index.js";
17
18
 
18
19
 
19
20
 
@@ -249,6 +250,22 @@ export default class ConfigValidator implements IConfigValidator {
249
250
  };
250
251
  }
251
252
 
253
+ if (res.options.defaultSort) {
254
+ const colName = res.options.defaultSort.columnName;
255
+ const col = res.columns.find((c) => c.name === colName);
256
+ if (!col) {
257
+ const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
258
+ errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
259
+ }
260
+ const dir = res.options.defaultSort.direction;
261
+ if (!dir) {
262
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
263
+ }
264
+ // AdminForthSortDirections is enum
265
+ if (!(Object.values(AdminForthSortDirections) as string[]).includes(dir)) {
266
+ errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
267
+ }
268
+ }
252
269
 
253
270
  if (Object.keys(res.options.allowedActions).includes('all')) {
254
271
  if (Object.keys(res.options.allowedActions).length > 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.54-next.17",
3
+ "version": "1.3.54-next.19",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -42,7 +42,8 @@
42
42
  "tailwindcss": "^3.4.3",
43
43
  "typescript": "~5.4.0",
44
44
  "vite": "^5.2.13",
45
- "vue-tsc": "^2.0.11"
45
+ "vue-tsc": "^2.0.11",
46
+ "vue3-json-viewer": "^2.2.2"
46
47
  }
47
48
  },
48
49
  "node_modules/@alloc/quick-lru": {
@@ -1785,6 +1786,18 @@
1785
1786
  "node": ">= 6"
1786
1787
  }
1787
1788
  },
1789
+ "node_modules/clipboard": {
1790
+ "version": "2.0.11",
1791
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
1792
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
1793
+ "dev": true,
1794
+ "license": "MIT",
1795
+ "dependencies": {
1796
+ "good-listener": "^1.2.2",
1797
+ "select": "^1.1.2",
1798
+ "tiny-emitter": "^2.0.0"
1799
+ }
1800
+ },
1788
1801
  "node_modules/color-convert": {
1789
1802
  "version": "2.0.1",
1790
1803
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1908,6 +1921,13 @@
1908
1921
  "node": ">=0.10.0"
1909
1922
  }
1910
1923
  },
1924
+ "node_modules/delegate": {
1925
+ "version": "3.2.0",
1926
+ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
1927
+ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
1928
+ "dev": true,
1929
+ "license": "MIT"
1930
+ },
1911
1931
  "node_modules/diacritics": {
1912
1932
  "version": "1.3.0",
1913
1933
  "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
@@ -2571,6 +2591,16 @@
2571
2591
  "url": "https://github.com/sponsors/sindresorhus"
2572
2592
  }
2573
2593
  },
2594
+ "node_modules/good-listener": {
2595
+ "version": "1.2.2",
2596
+ "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
2597
+ "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
2598
+ "dev": true,
2599
+ "license": "MIT",
2600
+ "dependencies": {
2601
+ "delegate": "^3.1.2"
2602
+ }
2603
+ },
2574
2604
  "node_modules/graphemer": {
2575
2605
  "version": "1.4.0",
2576
2606
  "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -3742,6 +3772,13 @@
3742
3772
  "node": ">=14.0.0"
3743
3773
  }
3744
3774
  },
3775
+ "node_modules/select": {
3776
+ "version": "1.1.2",
3777
+ "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
3778
+ "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
3779
+ "dev": true,
3780
+ "license": "MIT"
3781
+ },
3745
3782
  "node_modules/semver": {
3746
3783
  "version": "7.6.2",
3747
3784
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@@ -4047,6 +4084,13 @@
4047
4084
  "node": ">=0.8"
4048
4085
  }
4049
4086
  },
4087
+ "node_modules/tiny-emitter": {
4088
+ "version": "2.1.0",
4089
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
4090
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
4091
+ "dev": true,
4092
+ "license": "MIT"
4093
+ },
4050
4094
  "node_modules/to-regex-range": {
4051
4095
  "version": "5.0.1",
4052
4096
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4433,6 +4477,19 @@
4433
4477
  "typescript": "*"
4434
4478
  }
4435
4479
  },
4480
+ "node_modules/vue3-json-viewer": {
4481
+ "version": "2.2.2",
4482
+ "resolved": "https://registry.npmjs.org/vue3-json-viewer/-/vue3-json-viewer-2.2.2.tgz",
4483
+ "integrity": "sha512-56l3XDGggnpwEqZieXsSMhNT4NhtO6d7zuSAxHo4i0UVxymyY2jRb7UMQOU1ztChKALZCAzX7DlgrsnEhxu77A==",
4484
+ "dev": true,
4485
+ "license": "ISC",
4486
+ "dependencies": {
4487
+ "clipboard": "^2.0.10"
4488
+ },
4489
+ "peerDependencies": {
4490
+ "vue": "^3.2.0"
4491
+ }
4492
+ },
4436
4493
  "node_modules/which": {
4437
4494
  "version": "2.0.2",
4438
4495
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
package/spa/package.json CHANGED
@@ -46,6 +46,7 @@
46
46
  "tailwindcss": "^3.4.3",
47
47
  "typescript": "~5.4.0",
48
48
  "vite": "^5.2.13",
49
- "vue-tsc": "^2.0.11"
49
+ "vue-tsc": "^2.0.11",
50
+ "vue3-json-viewer": "^2.2.2"
50
51
  }
51
52
  }
package/spa/src/App.vue CHANGED
@@ -295,7 +295,7 @@ const theme = ref('light');
295
295
  function toggleTheme() {
296
296
  theme.value = theme.value === 'light' ? 'dark' : 'light';
297
297
  document.documentElement.classList.toggle('dark');
298
- window.localStorage.setItem('theme', theme.value);
298
+ window.localStorage.setItem('af__theme', theme.value);
299
299
 
300
300
  }
301
301
 
@@ -390,7 +390,7 @@ onMounted(async () => {
390
390
  })
391
391
 
392
392
  onBeforeMount(()=>{
393
- theme.value = window.localStorage.getItem('theme') || 'light';
393
+ theme.value = window.localStorage.getItem('af__theme') || 'light';
394
394
  document.documentElement.classList.toggle('dark', theme.value === 'dark');
395
395
  })
396
396
 
@@ -104,6 +104,14 @@
104
104
  @input="setCurrentValue(column.name, $event.target.value)"
105
105
  >
106
106
  </textarea>
107
+ <textarea
108
+ v-else-if="['json'].includes(column.type)"
109
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
110
+ placeholder="Text"
111
+ :value="currentValues[column.name]"
112
+ @input="setCurrentValue(column.name, $event.target.value)"
113
+ >
114
+ </textarea>
107
115
  <input
108
116
  v-else
109
117
  :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
@@ -155,7 +163,6 @@ import { useRouter, useRoute } from 'vue-router';
155
163
  const router = useRouter();
156
164
  const route = useRoute();
157
165
  const props = defineProps({
158
- loading: Boolean,
159
166
  resource: Object,
160
167
  record: Object,
161
168
  validating: Boolean,
@@ -193,6 +200,13 @@ const columnError = (column) => {
193
200
  ) {
194
201
  return 'This field is required';
195
202
  }
203
+ if (column.type === 'json' && currentValues.value[column.name]) {
204
+ try {
205
+ JSON.parse(currentValues.value[column.name]);
206
+ } catch (e) {
207
+ return 'Invalid JSON';
208
+ }
209
+ }
196
210
  if ( column.type === 'string' || column.type === 'text' ) {
197
211
  if ( column.maxLength && currentValues.value[column.name]?.length > column.maxLength ) {
198
212
  return `This field must be shorter than ${column.maxLength} characters`;
@@ -243,11 +257,32 @@ const setCurrentValue = (key, value) => {
243
257
  }
244
258
 
245
259
  currentValues.value = { ...currentValues.value };
246
- emit('update:record', currentValues.value);
260
+
261
+ //json fields should transform to object
262
+ const up = {...currentValues.value};
263
+ props.resource.columns.forEach((column) => {
264
+ if (column.type === 'json' && up[column.name]) {
265
+ try {
266
+ up[column.name] = JSON.parse(up[column.name]);
267
+ } catch (e) {
268
+ // do nothing
269
+ }
270
+ }
271
+ });
272
+ emit('update:record', up);
247
273
  };
248
274
 
249
275
  onMounted(() => {
276
+
250
277
  currentValues.value = Object.assign({}, props.record);
278
+ // json values should transform to string
279
+ props.resource.columns.forEach((column) => {
280
+ if (column.type === 'json' && currentValues.value[column.name]) {
281
+ currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
282
+ }
283
+ });
284
+ console.log('currentValues', currentValues.value);
285
+
251
286
  initFlowbite();
252
287
  emit('update:isValid', isValid.value);
253
288
  });
@@ -29,6 +29,9 @@
29
29
  <span v-else-if="column.type === 'richtext'">
30
30
  <div v-html="protectAgainstXSS(record[column.name])" class="allow-lists"></div>
31
31
  </span>
32
+ <span v-else-if="column.type === 'json'">
33
+ <JsonViewer :value="record[column.name]" copyable sort :theme="theme" />
34
+ </span>
32
35
  <span v-else>
33
36
  {{ checkEmptyValues(record[column.name],route.meta.type) }}
34
37
  </span>
@@ -44,13 +47,19 @@ import timezone from 'dayjs/plugin/timezone';
44
47
  import {checkEmptyValues} from '@/utils';
45
48
  import { useRoute, useRouter } from 'vue-router';
46
49
  import sanitizeHtml from 'sanitize-html';
50
+ import { JsonViewer } from "vue3-json-viewer";
51
+ import "vue3-json-viewer/dist/index.css";
47
52
 
48
53
 
49
54
  import { useCoreStore } from '@/stores/core';
55
+ import { computed } from 'vue';
50
56
 
51
57
  const coreStore = useCoreStore();
52
58
  const route = useRoute();
53
59
 
60
+ const theme = computed(() => {
61
+ return window.localStorage.getItem('af__theme') || 'light';
62
+ });
54
63
 
55
64
  dayjs.extend(utc);
56
65
  dayjs.extend(timezone);
@@ -111,4 +120,22 @@ function formatTime(time) {
111
120
  }
112
121
 
113
122
  }
123
+ </style>
124
+
125
+ <style lang="scss" >
126
+
127
+ .jv-container .jv-code {
128
+ padding: 10px 10px;
129
+ }
130
+
131
+ .jv-container .jv-button[class] {
132
+ @apply text-lightPrimary;
133
+ @apply dark:text-darkPrimary;
134
+
135
+ }
136
+
137
+ .jv-container.jv-dark {
138
+ background: transparent;
139
+ }
140
+
114
141
  </style>
@@ -90,7 +90,7 @@ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
90
90
  const isValid = ref(false);
91
91
  const validating = ref(false);
92
92
 
93
- const loading = ref(false);
93
+ const loading = ref(true);
94
94
  const saving = ref(false);
95
95
 
96
96
  const route = useRoute();
@@ -90,7 +90,7 @@ const validating = ref(false);
90
90
  const route = useRoute();
91
91
  const router = useRouter();
92
92
 
93
- const loading = ref(false);
93
+ const loading = ref(true);
94
94
 
95
95
  const saving = ref(false);
96
96