adminforth 1.3.54-next.2 → 1.3.54-next.20
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/dataConnectors/clickhouse.ts +11 -6
- package/dataConnectors/postgres.ts +5 -1
- package/dataConnectors/sqlite.ts +5 -1
- package/dist/dataConnectors/clickhouse.js +10 -5
- package/dist/dataConnectors/postgres.js +6 -1
- package/dist/dataConnectors/sqlite.js +6 -1
- package/dist/modules/codeInjector.js +15 -6
- package/dist/modules/configValidator.js +17 -0
- package/modules/codeInjector.ts +18 -7
- package/modules/configValidator.ts +17 -0
- package/package.json +4 -3
- package/spa/package-lock.json +87 -1
- package/spa/package.json +4 -1
- package/spa/src/App.vue +3 -3
- package/spa/src/components/ResourceForm.vue +37 -2
- package/spa/src/components/ResourceListTable.vue +11 -5
- package/spa/src/components/ValueRenderer.vue +27 -0
- package/spa/src/renderers/CompactUUID.vue +48 -0
- package/spa/src/renderers/CountryFlag.vue +69 -0
- package/spa/src/utils.ts +11 -0
- package/spa/src/views/CreateView.vue +1 -1
- package/spa/src/views/EditView.vue +1 -1
- package/spa/src/views/ListView.vue +78 -5
- package/spa/src/views/ShowView.vue +9 -1
|
@@ -2,6 +2,7 @@ import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirection
|
|
|
2
2
|
import AdminForthBaseConnector from './baseConnector.js';
|
|
3
3
|
import dayjs from 'dayjs';
|
|
4
4
|
import { createClient } from '@clickhouse/client'
|
|
5
|
+
import { base } from '@faker-js/faker';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
@@ -73,11 +74,11 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
73
74
|
field.scale = parseInt(scale);
|
|
74
75
|
} else if (baseType.startsWith('Float')) {
|
|
75
76
|
field.type = AdminForthDataTypes.FLOAT;
|
|
76
|
-
} else if (baseType == 'DateTime64' || baseType == 'DateTime') {
|
|
77
|
+
} else if (baseType == 'DateTime64' || baseType == 'DateTime' || baseType.startsWith('DateTime64(')) {
|
|
77
78
|
field.type = AdminForthDataTypes.DATETIME;
|
|
78
79
|
} else if (baseType == 'Date' || baseType == 'Date64') {
|
|
79
80
|
field.type = AdminForthDataTypes.DATE;
|
|
80
|
-
} else if (baseType == 'Boolean') {
|
|
81
|
+
} else if (baseType == 'Boolean' || baseType == 'Bool') {
|
|
81
82
|
field.type = AdminForthDataTypes.BOOLEAN;
|
|
82
83
|
field._underlineType = 'boolean';
|
|
83
84
|
} else {
|
|
@@ -117,7 +118,11 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
117
118
|
return !!value;
|
|
118
119
|
} else if (field.type == AdminForthDataTypes.JSON) {
|
|
119
120
|
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
|
|
120
|
-
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(value);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return {'error': `Failed to parse JSON: ${e.message}`}
|
|
125
|
+
}
|
|
121
126
|
} else {
|
|
122
127
|
console.error(`AdminForth: JSON field is not a string but ${field._underlineType}, this is not supported yet`);
|
|
123
128
|
}
|
|
@@ -265,14 +270,14 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
265
270
|
const tableName = resource.table;
|
|
266
271
|
const where = this.whereClause(resource, filters);
|
|
267
272
|
const d = this.whereParams(filters);
|
|
268
|
-
|
|
273
|
+
|
|
269
274
|
const countQ = await this.client.query({
|
|
270
|
-
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
275
|
+
query: `SELECT COUNT(*) as count FROM ${tableName} ${where}`,
|
|
271
276
|
format: 'JSONEachRow',
|
|
272
277
|
query_params: d,
|
|
273
278
|
});
|
|
274
279
|
const countResp = await countQ.json()
|
|
275
|
-
return countResp[0]['
|
|
280
|
+
return +countResp[0]['count'];
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource, columns: AdminForthResourceColumn[] }): Promise<{ [key: string]: { min: any, max: any } }> {
|
|
@@ -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
|
-
|
|
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 {
|
package/dataConnectors/sqlite.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -93,13 +93,13 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
93
93
|
else if (baseType.startsWith('Float')) {
|
|
94
94
|
field.type = AdminForthDataTypes.FLOAT;
|
|
95
95
|
}
|
|
96
|
-
else if (baseType == 'DateTime64' || baseType == 'DateTime') {
|
|
96
|
+
else if (baseType == 'DateTime64' || baseType == 'DateTime' || baseType.startsWith('DateTime64(')) {
|
|
97
97
|
field.type = AdminForthDataTypes.DATETIME;
|
|
98
98
|
}
|
|
99
99
|
else if (baseType == 'Date' || baseType == 'Date64') {
|
|
100
100
|
field.type = AdminForthDataTypes.DATE;
|
|
101
101
|
}
|
|
102
|
-
else if (baseType == 'Boolean') {
|
|
102
|
+
else if (baseType == 'Boolean' || baseType == 'Bool') {
|
|
103
103
|
field.type = AdminForthDataTypes.BOOLEAN;
|
|
104
104
|
field._underlineType = 'boolean';
|
|
105
105
|
}
|
|
@@ -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
|
-
|
|
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`);
|
|
@@ -247,12 +252,12 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
247
252
|
const where = this.whereClause(resource, filters);
|
|
248
253
|
const d = this.whereParams(filters);
|
|
249
254
|
const countQ = yield this.client.query({
|
|
250
|
-
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
255
|
+
query: `SELECT COUNT(*) as count FROM ${tableName} ${where}`,
|
|
251
256
|
format: 'JSONEachRow',
|
|
252
257
|
query_params: d,
|
|
253
258
|
});
|
|
254
259
|
const countResp = yield countQ.json();
|
|
255
|
-
return countResp[0]['
|
|
260
|
+
return +countResp[0]['count'];
|
|
256
261
|
});
|
|
257
262
|
}
|
|
258
263
|
getMinMaxForColumnsWithOriginalTypes(_a) {
|
|
@@ -154,7 +154,12 @@ class PostgresConnector extends AdminForthBaseConnector {
|
|
|
154
154
|
}
|
|
155
155
|
if (field.type == AdminForthDataTypes.JSON) {
|
|
156
156
|
if (typeof value == 'string') {
|
|
157
|
-
|
|
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
|
-
|
|
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`);
|
|
@@ -478,12 +478,21 @@ class CodeInjector {
|
|
|
478
478
|
process.env.HEAVY_DEBUG && console.log('🪲Hash file does not exist, proceeding with npm ci/install');
|
|
479
479
|
}
|
|
480
480
|
yield this.runNpmShell({ command: 'ci', cwd: CodeInjector.SPA_TMP_PATH });
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
481
|
+
const allPacks = [
|
|
482
|
+
...iconPackageNames,
|
|
483
|
+
...usersPackages,
|
|
484
|
+
...pluginPackages.reduce((acc, { packages }) => {
|
|
485
|
+
acc.push(...packages);
|
|
486
|
+
return acc;
|
|
487
|
+
}, []),
|
|
488
|
+
];
|
|
489
|
+
const EXCLUDE_PACKS = ['@iconify-prerendered/vue-flowbite'];
|
|
490
|
+
const allPacksFiltered = allPacks.filter((pack) => {
|
|
491
|
+
return !EXCLUDE_PACKS.some((exclude) => pack.startsWith(exclude));
|
|
492
|
+
});
|
|
493
|
+
const allPacksUnique = Array.from(new Set(allPacksFiltered));
|
|
494
|
+
if (allPacks.length) {
|
|
495
|
+
const npmInstallCommand = `install ${allPacksUnique.join(' ')}`;
|
|
487
496
|
yield this.runNpmShell({ command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH });
|
|
488
497
|
}
|
|
489
498
|
yield fs.promises.writeFile(hashPath, fullHash);
|
|
@@ -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(', ')}`);
|
package/modules/codeInjector.ts
CHANGED
|
@@ -551,13 +551,24 @@ class CodeInjector implements ICodeInjector {
|
|
|
551
551
|
}
|
|
552
552
|
|
|
553
553
|
await this.runNpmShell({command: 'ci', cwd: CodeInjector.SPA_TMP_PATH});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
554
|
+
|
|
555
|
+
const allPacks = [
|
|
556
|
+
...iconPackageNames,
|
|
557
|
+
...usersPackages,
|
|
558
|
+
...pluginPackages.reduce((acc, { packages }) => {
|
|
559
|
+
acc.push(...packages);
|
|
560
|
+
return acc;
|
|
561
|
+
}, []),
|
|
562
|
+
];
|
|
563
|
+
const EXCLUDE_PACKS = ['@iconify-prerendered/vue-flowbite'];
|
|
564
|
+
|
|
565
|
+
const allPacksFiltered = allPacks.filter((pack) => {
|
|
566
|
+
return !EXCLUDE_PACKS.some((exclude) => pack.startsWith(exclude));
|
|
567
|
+
})
|
|
568
|
+
const allPacksUnique = Array.from(new Set(allPacksFiltered));
|
|
569
|
+
|
|
570
|
+
if (allPacks.length) {
|
|
571
|
+
const npmInstallCommand = `install ${allPacksUnique.join(' ')}`;
|
|
561
572
|
await this.runNpmShell({command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH});
|
|
562
573
|
}
|
|
563
574
|
|
|
@@ -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.
|
|
3
|
+
"version": "1.3.54-next.20",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"---prepareDist": "cp -rL spa dist/",
|
|
11
|
-
"
|
|
12
|
-
"rollout
|
|
11
|
+
"put-git-tag": "git tag v$(node -p \"require('./package.json').version\") && git push --tags",
|
|
12
|
+
"rollout": "tsc && npm version patch && npm publish && npm run rollout-doc && npm run put-git-tag",
|
|
13
|
+
"rollout-next": "tsc && npm version prerelease --preid=next && npm publish --tag next && npm run put-git-tag",
|
|
13
14
|
"rollout-doc": "cd documentation && npm run build && npm run deploy",
|
|
14
15
|
"docs": "typedoc",
|
|
15
16
|
"postinstall": "cd ./spa/src/ && test ! -d ./types && ln -sf ../../types ./types || echo 'types linked'"
|
package/spa/package-lock.json
CHANGED
|
@@ -34,13 +34,16 @@
|
|
|
34
34
|
"autoprefixer": "^10.4.19",
|
|
35
35
|
"eslint": "^8.57.0",
|
|
36
36
|
"eslint-plugin-vue": "^9.23.0",
|
|
37
|
+
"flag-icons": "^7.2.3",
|
|
38
|
+
"i18n-iso-countries": "^7.12.0",
|
|
37
39
|
"npm-run-all2": "^6.1.2",
|
|
38
40
|
"postcss": "^8.4.38",
|
|
39
41
|
"sass": "^1.77.2",
|
|
40
42
|
"tailwindcss": "^3.4.3",
|
|
41
43
|
"typescript": "~5.4.0",
|
|
42
44
|
"vite": "^5.2.13",
|
|
43
|
-
"vue-tsc": "^2.0.11"
|
|
45
|
+
"vue-tsc": "^2.0.11",
|
|
46
|
+
"vue3-json-viewer": "^2.2.2"
|
|
44
47
|
}
|
|
45
48
|
},
|
|
46
49
|
"node_modules/@alloc/quick-lru": {
|
|
@@ -1783,6 +1786,18 @@
|
|
|
1783
1786
|
"node": ">= 6"
|
|
1784
1787
|
}
|
|
1785
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
|
+
},
|
|
1786
1801
|
"node_modules/color-convert": {
|
|
1787
1802
|
"version": "2.0.1",
|
|
1788
1803
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
@@ -1906,6 +1921,20 @@
|
|
|
1906
1921
|
"node": ">=0.10.0"
|
|
1907
1922
|
}
|
|
1908
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
|
+
},
|
|
1931
|
+
"node_modules/diacritics": {
|
|
1932
|
+
"version": "1.3.0",
|
|
1933
|
+
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
|
1934
|
+
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
|
|
1935
|
+
"dev": true,
|
|
1936
|
+
"license": "MIT"
|
|
1937
|
+
},
|
|
1909
1938
|
"node_modules/didyoumean": {
|
|
1910
1939
|
"version": "1.2.2",
|
|
1911
1940
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
|
@@ -2371,6 +2400,13 @@
|
|
|
2371
2400
|
"url": "https://github.com/sponsors/sindresorhus"
|
|
2372
2401
|
}
|
|
2373
2402
|
},
|
|
2403
|
+
"node_modules/flag-icons": {
|
|
2404
|
+
"version": "7.2.3",
|
|
2405
|
+
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.3.tgz",
|
|
2406
|
+
"integrity": "sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg==",
|
|
2407
|
+
"dev": true,
|
|
2408
|
+
"license": "MIT"
|
|
2409
|
+
},
|
|
2374
2410
|
"node_modules/flat-cache": {
|
|
2375
2411
|
"version": "3.2.0",
|
|
2376
2412
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
|
@@ -2555,6 +2591,16 @@
|
|
|
2555
2591
|
"url": "https://github.com/sponsors/sindresorhus"
|
|
2556
2592
|
}
|
|
2557
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
|
+
},
|
|
2558
2604
|
"node_modules/graphemer": {
|
|
2559
2605
|
"version": "1.4.0",
|
|
2560
2606
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
|
@@ -2622,6 +2668,19 @@
|
|
|
2622
2668
|
"entities": "^4.4.0"
|
|
2623
2669
|
}
|
|
2624
2670
|
},
|
|
2671
|
+
"node_modules/i18n-iso-countries": {
|
|
2672
|
+
"version": "7.12.0",
|
|
2673
|
+
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz",
|
|
2674
|
+
"integrity": "sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw==",
|
|
2675
|
+
"dev": true,
|
|
2676
|
+
"license": "MIT",
|
|
2677
|
+
"dependencies": {
|
|
2678
|
+
"diacritics": "1.3.0"
|
|
2679
|
+
},
|
|
2680
|
+
"engines": {
|
|
2681
|
+
"node": ">= 12"
|
|
2682
|
+
}
|
|
2683
|
+
},
|
|
2625
2684
|
"node_modules/ignore": {
|
|
2626
2685
|
"version": "5.3.1",
|
|
2627
2686
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
|
@@ -3713,6 +3772,13 @@
|
|
|
3713
3772
|
"node": ">=14.0.0"
|
|
3714
3773
|
}
|
|
3715
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
|
+
},
|
|
3716
3782
|
"node_modules/semver": {
|
|
3717
3783
|
"version": "7.6.2",
|
|
3718
3784
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
|
@@ -4018,6 +4084,13 @@
|
|
|
4018
4084
|
"node": ">=0.8"
|
|
4019
4085
|
}
|
|
4020
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
|
+
},
|
|
4021
4094
|
"node_modules/to-regex-range": {
|
|
4022
4095
|
"version": "5.0.1",
|
|
4023
4096
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
@@ -4404,6 +4477,19 @@
|
|
|
4404
4477
|
"typescript": "*"
|
|
4405
4478
|
}
|
|
4406
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
|
+
},
|
|
4407
4493
|
"node_modules/which": {
|
|
4408
4494
|
"version": "2.0.2",
|
|
4409
4495
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
package/spa/package.json
CHANGED
|
@@ -38,12 +38,15 @@
|
|
|
38
38
|
"autoprefixer": "^10.4.19",
|
|
39
39
|
"eslint": "^8.57.0",
|
|
40
40
|
"eslint-plugin-vue": "^9.23.0",
|
|
41
|
+
"flag-icons": "^7.2.3",
|
|
42
|
+
"i18n-iso-countries": "^7.12.0",
|
|
41
43
|
"npm-run-all2": "^6.1.2",
|
|
42
44
|
"postcss": "^8.4.38",
|
|
43
45
|
"sass": "^1.77.2",
|
|
44
46
|
"tailwindcss": "^3.4.3",
|
|
45
47
|
"typescript": "~5.4.0",
|
|
46
48
|
"vite": "^5.2.13",
|
|
47
|
-
"vue-tsc": "^2.0.11"
|
|
49
|
+
"vue-tsc": "^2.0.11",
|
|
50
|
+
"vue3-json-viewer": "^2.2.2"
|
|
48
51
|
}
|
|
49
52
|
}
|
package/spa/src/App.vue
CHANGED
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
</aside>
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
<div class="sm:ml-64" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
|
|
176
|
+
<div class="sm:ml-64 max-w-[100vw]" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
|
|
177
177
|
<div class="p-0 dark:border-gray-700 mt-14">
|
|
178
178
|
<RouterView/>
|
|
179
179
|
</div>
|
|
@@ -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('
|
|
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('
|
|
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
|
-
|
|
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
|
});
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
<script setup>
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
import { computed, ref, watch } from 'vue';
|
|
253
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
254
254
|
import { callAdminForthApi } from '@/utils';
|
|
255
255
|
|
|
256
256
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
@@ -260,19 +260,20 @@ import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
|
|
|
260
260
|
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
261
261
|
|
|
262
262
|
import {
|
|
263
|
-
IconInboxOutline,
|
|
263
|
+
IconInboxOutline,
|
|
264
264
|
} from '@iconify-prerendered/vue-flowbite';
|
|
265
265
|
|
|
266
266
|
import {
|
|
267
|
-
IconEyeSolid,
|
|
268
|
-
IconPenSolid,
|
|
269
|
-
IconTrashBinSolid
|
|
267
|
+
IconEyeSolid,
|
|
268
|
+
IconPenSolid,
|
|
269
|
+
IconTrashBinSolid
|
|
270
270
|
} from '@iconify-prerendered/vue-flowbite';
|
|
271
271
|
import router from '@/router';
|
|
272
272
|
|
|
273
273
|
const coreStore = useCoreStore();
|
|
274
274
|
|
|
275
275
|
const props = defineProps([
|
|
276
|
+
'page',
|
|
276
277
|
'resource',
|
|
277
278
|
'rows',
|
|
278
279
|
'totalRows',
|
|
@@ -296,6 +297,7 @@ const page = ref(1);
|
|
|
296
297
|
const sort = ref([]);
|
|
297
298
|
|
|
298
299
|
|
|
300
|
+
|
|
299
301
|
watch(() => page.value, (newPage) => {
|
|
300
302
|
emits('update:page', newPage);
|
|
301
303
|
});
|
|
@@ -319,6 +321,10 @@ watch(() => props.sort, (newSort) => {
|
|
|
319
321
|
sort.value = newSort;
|
|
320
322
|
});
|
|
321
323
|
|
|
324
|
+
watch(() => props.page, (newPage) => {
|
|
325
|
+
page.value = newPage;
|
|
326
|
+
});
|
|
327
|
+
|
|
322
328
|
function addToCheckedValues(id) {
|
|
323
329
|
console.log('checking', checkboxesInternal.value, 'id', id)
|
|
324
330
|
if (checkboxesInternal.value.includes(id)) {
|
|
@@ -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>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="flex items-center"
|
|
3
|
+
:data-tooltip-target="val && `tooltip-${id}`"
|
|
4
|
+
data-tooltip-placement="top"
|
|
5
|
+
>
|
|
6
|
+
{{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="val"/>
|
|
7
|
+
|
|
8
|
+
<div :id="`tooltip-${id}`" role="tooltip" v-if="val"
|
|
9
|
+
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
|
|
10
|
+
{{ props.record[props.column.name] }}
|
|
11
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
12
|
+
</div>
|
|
13
|
+
</span>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import { computed, ref, onMounted } from 'vue';
|
|
18
|
+
import { IconFileCopyAltSolid } from '@iconify-prerendered/vue-flowbite';
|
|
19
|
+
import { initFlowbite } from 'flowbite';
|
|
20
|
+
|
|
21
|
+
const visualValue = computed(() => {
|
|
22
|
+
// if lenght is more then 8, show only first 4 and last 4 characters, ... in the middle
|
|
23
|
+
const val = props.record[props.column.name];
|
|
24
|
+
if (val && val.length > 8) {
|
|
25
|
+
return `${val.substr(0, 4)}...${val.substr(val.length - 4)}`;
|
|
26
|
+
}
|
|
27
|
+
return val;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const props = defineProps(['column', 'record', 'meta']);
|
|
31
|
+
|
|
32
|
+
const id = ref();
|
|
33
|
+
|
|
34
|
+
function copyToCB() {
|
|
35
|
+
navigator.clipboard.writeText(props.record[props.column.name]);
|
|
36
|
+
window.adminforth.alert({
|
|
37
|
+
message: 'ID copied to clipboard',
|
|
38
|
+
variant: 'success',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onMounted(async () => {
|
|
43
|
+
id.value = Math.random().toString(36).substring(7);
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
45
|
+
initFlowbite();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
</script>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="flex items-center">
|
|
3
|
+
<span
|
|
4
|
+
:class="{[`fi-${countryIsoLow}`]: true, 'flag-icon': countryName}"
|
|
5
|
+
:data-tooltip-target="`tooltip-${id}`"
|
|
6
|
+
></span>
|
|
7
|
+
|
|
8
|
+
<span v-if="meta.showCountryName" class="ms-2">{{ countryName }}</span>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-if="!meta.showCountryName && countryName"
|
|
12
|
+
:id="`tooltip-${id}`" role="tooltip"
|
|
13
|
+
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
|
|
14
|
+
>
|
|
15
|
+
{{ countryName }}
|
|
16
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
17
|
+
</div>
|
|
18
|
+
</span>
|
|
19
|
+
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup>
|
|
23
|
+
|
|
24
|
+
import { computed, ref, onMounted } from 'vue';
|
|
25
|
+
import { initFlowbite } from 'flowbite';
|
|
26
|
+
import 'flag-icons/css/flag-icons.min.css';
|
|
27
|
+
import isoCountries from 'i18n-iso-countries';
|
|
28
|
+
import enLocal from 'i18n-iso-countries/langs/en.json';
|
|
29
|
+
|
|
30
|
+
isoCountries.registerLocale(enLocal);
|
|
31
|
+
|
|
32
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
33
|
+
|
|
34
|
+
const id = ref();
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
onMounted(async () => {
|
|
38
|
+
id.value = Math.random().toString(36).substring(7);
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
40
|
+
initFlowbite();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const countryIsoLow = computed(() => {
|
|
44
|
+
return props.record[props.column.name]?.toLowerCase();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const countryName = computed(() => {
|
|
48
|
+
if (!countryIsoLow.value) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
return isoCountries.getName(countryIsoLow.value, 'en');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style scoped lang="scss">
|
|
57
|
+
|
|
58
|
+
.flag-icon {
|
|
59
|
+
width: 2rem;
|
|
60
|
+
height: 1.5rem;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
|
|
63
|
+
// border radius for background
|
|
64
|
+
border-radius: 2px;
|
|
65
|
+
// add some silkiness to the flag
|
|
66
|
+
box-shadow: inset -1px -1px 0.5px 0px rgba(0 0 0 / 0.2), inset 1px 1px 0.5px 0px rgba(255 255 255 / 0.2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
</style>
|
package/spa/src/utils.ts
CHANGED
|
@@ -146,4 +146,15 @@ export function applyRegexValidation(value: any, validation: ValidationObject[]
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function currentQuery() {
|
|
152
|
+
return router.currentRoute.value.query;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function setQuery(query: any) {
|
|
156
|
+
const currentQuery = { ...router.currentRoute.value.query, ...query };
|
|
157
|
+
router.replace({
|
|
158
|
+
query: currentQuery,
|
|
159
|
+
});
|
|
149
160
|
}
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
@click="()=>{checkboxes = []}"
|
|
22
22
|
v-if="checkboxes.length"
|
|
23
23
|
data-tooltip-target="tooltip-remove-all"
|
|
24
|
-
data-tooltip-placement="bottom"
|
|
25
24
|
class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
26
25
|
>
|
|
27
26
|
<IconBanOutline class="w-5 h-5 "/>
|
|
@@ -89,6 +88,7 @@
|
|
|
89
88
|
<ResourceListTable
|
|
90
89
|
:resource="coreStore.resource"
|
|
91
90
|
:rows="rows"
|
|
91
|
+
:page="page"
|
|
92
92
|
@update:page="page = $event"
|
|
93
93
|
@update:sort="sort = $event"
|
|
94
94
|
@update:checkboxes="checkboxes = $event"
|
|
@@ -115,7 +115,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
115
115
|
import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
116
116
|
import { useCoreStore } from '@/stores/core';
|
|
117
117
|
import { useFiltersStore } from '@/stores/filters';
|
|
118
|
-
import { callAdminForthApi, getIcon } from '@/utils';
|
|
118
|
+
import { callAdminForthApi, currentQuery, getIcon, setQuery } from '@/utils';
|
|
119
119
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
120
120
|
import { useRoute } from 'vue-router';
|
|
121
121
|
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
@@ -227,6 +227,18 @@ async function startBulkAction(actionId) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
class SortQuerySerializer {
|
|
231
|
+
static serialize(sort) {
|
|
232
|
+
return sort.map(s => `${s.field}__${s.direction}`).join(',');
|
|
233
|
+
}
|
|
234
|
+
static deserialize(str) {
|
|
235
|
+
return str.split(',').map(s => {
|
|
236
|
+
const [field, direction] = s.split('__');
|
|
237
|
+
return { field, direction };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
230
242
|
|
|
231
243
|
async function init() {
|
|
232
244
|
|
|
@@ -236,9 +248,26 @@ async function init() {
|
|
|
236
248
|
|
|
237
249
|
initFlowbite();
|
|
238
250
|
|
|
239
|
-
// !!! clear filters should be in same tick with sort assignment so that watch can catch it
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
// !!! clear filters should be in same tick with sort assignment so that watch can catch it as one change
|
|
252
|
+
|
|
253
|
+
// try to init filters from query params
|
|
254
|
+
const filters = Object.keys(route.query).filter(k => k.startsWith('filter__')).map(k => {
|
|
255
|
+
const [_, field, operator] = k.split('__');
|
|
256
|
+
return {
|
|
257
|
+
field,
|
|
258
|
+
operator,
|
|
259
|
+
value: JSON.parse(decodeURIComponent(route.query[k]))
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (filters.length) {
|
|
263
|
+
filtersStore.setFilters(filters);
|
|
264
|
+
} else {
|
|
265
|
+
filtersStore.clearFilters();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (route.query.sort) {
|
|
269
|
+
sort.value = SortQuerySerializer.deserialize(route.query.sort);
|
|
270
|
+
} else if (coreStore.resource.options?.defaultSort) {
|
|
242
271
|
sort.value = [{
|
|
243
272
|
field: coreStore.resource.options.defaultSort.columnName,
|
|
244
273
|
direction: coreStore.resource.options.defaultSort.direction
|
|
@@ -246,6 +275,11 @@ async function init() {
|
|
|
246
275
|
} else {
|
|
247
276
|
sort.value = [];
|
|
248
277
|
}
|
|
278
|
+
// page init should be also in same tick
|
|
279
|
+
if (route.query.page) {
|
|
280
|
+
page.value = parseInt(route.query.page);
|
|
281
|
+
}
|
|
282
|
+
|
|
249
283
|
// await getList(); - Not needed here, watch will trigger it
|
|
250
284
|
columnsMinMax.value = await callAdminForthApi({
|
|
251
285
|
path: '/get_min_max_for_columns',
|
|
@@ -257,6 +291,8 @@ async function init() {
|
|
|
257
291
|
}
|
|
258
292
|
|
|
259
293
|
watch([page, sort, () => filtersStore.filters], async () => {
|
|
294
|
+
console.log('🔄️ page/sort/filter change fired, page:', page.value);
|
|
295
|
+
|
|
260
296
|
await getList();
|
|
261
297
|
}, { deep: true });
|
|
262
298
|
|
|
@@ -264,16 +300,53 @@ window.adminforth.list.refresh = async () => {
|
|
|
264
300
|
await getList();
|
|
265
301
|
}
|
|
266
302
|
|
|
303
|
+
let initInProcess = false;
|
|
304
|
+
|
|
267
305
|
watch(() => filtersStore.filters, async (to, from) => {
|
|
306
|
+
if (initInProcess) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log('🔄️ filters changed', JSON.stringify(to))
|
|
268
310
|
page.value = 1;
|
|
269
311
|
checkboxes.value = [];
|
|
312
|
+
// update query param for each filter as filter_<column_name>=value
|
|
313
|
+
const query = {};
|
|
314
|
+
const currentQ = currentQuery();
|
|
315
|
+
filtersStore.filters.forEach(f => {
|
|
316
|
+
if (f.value) {
|
|
317
|
+
query[`filter__${f.field}__${f.operator}`] = encodeURIComponent(JSON.stringify(f.value));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
// set every key in currentQ which starts with filter_ to undefined if it is not in query
|
|
321
|
+
Object.keys(currentQ).forEach(k => {
|
|
322
|
+
if (k.startsWith('filter_') && !query[k]) {
|
|
323
|
+
query[k] = undefined;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
setQuery(query);
|
|
270
327
|
}, {deep: true});
|
|
271
328
|
|
|
272
329
|
onMounted(async () => {
|
|
330
|
+
initInProcess = true;
|
|
273
331
|
await init();
|
|
274
332
|
initThreeDotsDropdown();
|
|
333
|
+
initInProcess = false;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
watch([page], async () => {
|
|
337
|
+
setQuery({ page: page.value });
|
|
275
338
|
});
|
|
276
339
|
|
|
277
340
|
|
|
278
341
|
|
|
342
|
+
|
|
343
|
+
watch([sort], async () => {
|
|
344
|
+
if (!sort.value.length) {
|
|
345
|
+
setQuery({ sort: undefined });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
setQuery({ sort: SortQuerySerializer.serialize(sort.value) });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
|
|
279
352
|
</script>
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
:adminUser="coreStore.adminUser"
|
|
11
11
|
/>
|
|
12
12
|
<BreadcrumbsWithButtons>
|
|
13
|
+
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
|
|
14
|
+
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
|
|
15
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
16
|
+
>
|
|
17
|
+
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
18
|
+
Add new
|
|
19
|
+
</RouterLink>
|
|
20
|
+
|
|
13
21
|
<RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
|
|
14
22
|
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
15
23
|
>
|
|
@@ -126,7 +134,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
126
134
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
127
135
|
import { useCoreStore } from '@/stores/core';
|
|
128
136
|
import { getCustomComponent, checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
|
|
129
|
-
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
137
|
+
import { IconPenSolid, IconTrashBinSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
|
|
130
138
|
import { onMounted, ref } from 'vue';
|
|
131
139
|
import { useRoute,useRouter } from 'vue-router';
|
|
132
140
|
import {callAdminForthApi} from '@/utils';
|