adminforth 1.2.22 → 1.2.24

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.
@@ -1,11 +1,13 @@
1
1
  import dayjs from 'dayjs';
2
2
  import { MongoClient } from 'mongodb';
3
3
  import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, IAdminForthDataSourceConnector } from '../types/AdminForthConfig.js';
4
+ import AdminForthBaseConnector from './baseConnector.js';
4
5
 
5
- class MongoConnector implements IAdminForthDataSourceConnector {
6
+ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
6
7
  db: MongoClient
7
8
 
8
9
  constructor({ url }: { url: string }) {
10
+ super();
9
11
  this.db = new MongoClient(url);
10
12
  (async () => {
11
13
  try {
@@ -10,8 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import dayjs from 'dayjs';
11
11
  import { MongoClient } from 'mongodb';
12
12
  import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from '../types/AdminForthConfig.js';
13
- class MongoConnector {
13
+ import AdminForthBaseConnector from './baseConnector.js';
14
+ class MongoConnector extends AdminForthBaseConnector {
14
15
  constructor({ url }) {
16
+ super();
15
17
  this.OperatorsMap = {
16
18
  [AdminForthFilterOperators.EQ]: (value) => value,
17
19
  [AdminForthFilterOperators.NE]: (value) => ({ $ne: value }),
@@ -404,6 +404,7 @@ class CodeInjector {
404
404
  const pluginPackages = [];
405
405
  // for every installed plugin generate packages
406
406
  for (const plugin of this.adminforth.activatedPlugins) {
407
+ console.log('🔧 Checking packages for plugin', plugin.customFolderPath);
407
408
  const [lockHash, packages] = yield this.packagesFromNpm(plugin.customFolderPath);
408
409
  if (packages.length) {
409
410
  pluginPackages.push({
@@ -405,5 +405,10 @@ export default class ConfigValidator {
405
405
  }
406
406
  });
407
407
  resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
408
+ (resource.plugins || []).forEach((plugin) => {
409
+ if (plugin.validateConfigAfterDiscover) {
410
+ plugin.validateConfigAfterDiscover(this.adminforth, resource);
411
+ }
412
+ });
408
413
  }
409
414
  }
@@ -16,6 +16,7 @@
16
16
  "flowbite": "^2.3.0",
17
17
  "flowbite-datepicker": "^1.2.6",
18
18
  "pinia": "^2.1.7",
19
+ "sanitize-html": "^2.13.0",
19
20
  "unhead": "^1.9.12",
20
21
  "uuid": "^10.0.0",
21
22
  "vue": "^3.4.21",
@@ -1897,6 +1898,14 @@
1897
1898
  "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1898
1899
  "dev": true
1899
1900
  },
1901
+ "node_modules/deepmerge": {
1902
+ "version": "4.3.1",
1903
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
1904
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
1905
+ "engines": {
1906
+ "node": ">=0.10.0"
1907
+ }
1908
+ },
1900
1909
  "node_modules/didyoumean": {
1901
1910
  "version": "1.2.2",
1902
1911
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -1938,6 +1947,57 @@
1938
1947
  "node": ">=6.0.0"
1939
1948
  }
1940
1949
  },
1950
+ "node_modules/dom-serializer": {
1951
+ "version": "2.0.0",
1952
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
1953
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
1954
+ "dependencies": {
1955
+ "domelementtype": "^2.3.0",
1956
+ "domhandler": "^5.0.2",
1957
+ "entities": "^4.2.0"
1958
+ },
1959
+ "funding": {
1960
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
1961
+ }
1962
+ },
1963
+ "node_modules/domelementtype": {
1964
+ "version": "2.3.0",
1965
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
1966
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
1967
+ "funding": [
1968
+ {
1969
+ "type": "github",
1970
+ "url": "https://github.com/sponsors/fb55"
1971
+ }
1972
+ ]
1973
+ },
1974
+ "node_modules/domhandler": {
1975
+ "version": "5.0.3",
1976
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
1977
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
1978
+ "dependencies": {
1979
+ "domelementtype": "^2.3.0"
1980
+ },
1981
+ "engines": {
1982
+ "node": ">= 4"
1983
+ },
1984
+ "funding": {
1985
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
1986
+ }
1987
+ },
1988
+ "node_modules/domutils": {
1989
+ "version": "3.1.0",
1990
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
1991
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
1992
+ "dependencies": {
1993
+ "dom-serializer": "^2.0.0",
1994
+ "domelementtype": "^2.3.0",
1995
+ "domhandler": "^5.0.3"
1996
+ },
1997
+ "funding": {
1998
+ "url": "https://github.com/fb55/domutils?sponsor=1"
1999
+ }
2000
+ },
1941
2001
  "node_modules/eastasianwidth": {
1942
2002
  "version": "0.2.0",
1943
2003
  "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2018,7 +2078,6 @@
2018
2078
  "version": "4.0.0",
2019
2079
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
2020
2080
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
2021
- "dev": true,
2022
2081
  "engines": {
2023
2082
  "node": ">=10"
2024
2083
  },
@@ -2545,6 +2604,24 @@
2545
2604
  "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
2546
2605
  "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
2547
2606
  },
2607
+ "node_modules/htmlparser2": {
2608
+ "version": "8.0.2",
2609
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
2610
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
2611
+ "funding": [
2612
+ "https://github.com/fb55/htmlparser2?sponsor=1",
2613
+ {
2614
+ "type": "github",
2615
+ "url": "https://github.com/sponsors/fb55"
2616
+ }
2617
+ ],
2618
+ "dependencies": {
2619
+ "domelementtype": "^2.3.0",
2620
+ "domhandler": "^5.0.3",
2621
+ "domutils": "^3.0.1",
2622
+ "entities": "^4.4.0"
2623
+ }
2624
+ },
2548
2625
  "node_modules/ignore": {
2549
2626
  "version": "5.3.1",
2550
2627
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2673,6 +2750,14 @@
2673
2750
  "node": ">=8"
2674
2751
  }
2675
2752
  },
2753
+ "node_modules/is-plain-object": {
2754
+ "version": "5.0.0",
2755
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
2756
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
2757
+ "engines": {
2758
+ "node": ">=0.10.0"
2759
+ }
2760
+ },
2676
2761
  "node_modules/isexe": {
2677
2762
  "version": "2.0.0",
2678
2763
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3103,6 +3188,11 @@
3103
3188
  "node": ">=6"
3104
3189
  }
3105
3190
  },
3191
+ "node_modules/parse-srcset": {
3192
+ "version": "1.0.2",
3193
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
3194
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
3195
+ },
3106
3196
  "node_modules/path-browserify": {
3107
3197
  "version": "1.0.1",
3108
3198
  "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -3593,6 +3683,19 @@
3593
3683
  "queue-microtask": "^1.2.2"
3594
3684
  }
3595
3685
  },
3686
+ "node_modules/sanitize-html": {
3687
+ "version": "2.13.0",
3688
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz",
3689
+ "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==",
3690
+ "dependencies": {
3691
+ "deepmerge": "^4.2.2",
3692
+ "escape-string-regexp": "^4.0.0",
3693
+ "htmlparser2": "^8.0.0",
3694
+ "is-plain-object": "^5.0.0",
3695
+ "parse-srcset": "^1.0.2",
3696
+ "postcss": "^8.3.11"
3697
+ }
3698
+ },
3596
3699
  "node_modules/sass": {
3597
3700
  "version": "1.77.2",
3598
3701
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
@@ -20,6 +20,7 @@
20
20
  "flowbite": "^2.3.0",
21
21
  "flowbite-datepicker": "^1.2.6",
22
22
  "pinia": "^2.1.7",
23
+ "sanitize-html": "^2.13.0",
23
24
  "unhead": "^1.9.12",
24
25
  "uuid": "^10.0.0",
25
26
  "vue": "^3.4.21",
@@ -189,8 +189,6 @@ function setFilterItem({ column, operator, value }) {
189
189
  if (index !== -1) {
190
190
  filtersStore.filters.splice(index, 1);
191
191
  }
192
- emits('update:filters', [...filtersStore.filters]);
193
- return;
194
192
  } else {
195
193
  if (index === -1) {
196
194
  filtersStore.setFilter({ field: column.name, value, operator });
@@ -207,5 +205,6 @@ function getFilterItem({ column, operator }) {
207
205
 
208
206
  async function clear() {
209
207
  filtersStore.clearFilters();
208
+ emits('update:filters', [...filtersStore.filters]);
210
209
  }
211
210
  </script>
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <div class="rounded-default">
3
-
4
3
  <div
5
4
  class="relative shadow-resourseFormShadow sm:rounded-lg dark:shadow-2xl dark:shadow-black rounded-default"
6
5
  >
@@ -44,6 +43,7 @@
44
43
  :meta="column.components[props.source].meta"
45
44
  :record="props.record"
46
45
  @update:inValidity="customComponentsInValidity[column.name] = $event"
46
+ @update:emptiness="customComponentsEmptiness[column.name] = $event"
47
47
  />
48
48
  </template>
49
49
  <template v-else>
@@ -95,7 +95,7 @@
95
95
  @input="setCurrentValue(column.name, $event.target.value)"
96
96
  />
97
97
  <textarea
98
- v-else-if="['text'].includes(column.type)"
98
+ v-else-if="['text', 'richtext'].includes(column.type)"
99
99
  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"
100
100
  placeholder="Text"
101
101
  :value="currentValues[column.name]"
@@ -171,6 +171,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
171
171
  const currentValues = ref({});
172
172
 
173
173
  const customComponentsInValidity = ref({});
174
+ const customComponentsEmptiness = ref({});
174
175
 
175
176
 
176
177
  const columnError = (column) => {
@@ -179,7 +180,14 @@ const columnError = (column) => {
179
180
  return customComponentsInValidity.value[column.name];
180
181
  }
181
182
 
182
- if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
183
+ if (
184
+ column.required[mode.value] &&
185
+ (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
186
+ // if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
187
+ // components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
188
+ (customComponentsEmptiness.value[column.name] !== false)
189
+
190
+ ) {
183
191
  return 'This field is required';
184
192
  }
185
193
  if ( column.type === 'string' || column.type === 'text' ) {
@@ -20,6 +20,9 @@
20
20
  <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
21
21
  {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
22
22
  </span>
23
+ <span v-else-if="column.type === 'richtext'">
24
+ <div v-html="protectAgainstXSS(record[column.name])"></div>
25
+ </span>
23
26
  <span v-else>
24
27
  {{ checkEmptyValues(record[column.name],route.meta.type) }}
25
28
  </span>
@@ -34,6 +37,7 @@ import utc from 'dayjs/plugin/utc';
34
37
  import timezone from 'dayjs/plugin/timezone';
35
38
  import {checkEmptyValues} from '@/utils';
36
39
  import { useRoute, useRouter } from 'vue-router';
40
+ import sanitizeHtml from 'sanitize-html';
37
41
 
38
42
 
39
43
  import { useCoreStore } from '@/stores/core';
@@ -50,6 +54,10 @@ const props = defineProps({
50
54
  record: Object
51
55
  });
52
56
 
57
+ function protectAgainstXSS(value) {
58
+ return sanitizeHtml(value);
59
+ }
60
+
53
61
 
54
62
  function formatDate(date) {
55
63
  if (!date) return '';
@@ -84,6 +84,7 @@ export var AdminForthDataTypes;
84
84
  AdminForthDataTypes["TIME"] = "time";
85
85
  AdminForthDataTypes["TEXT"] = "text";
86
86
  AdminForthDataTypes["JSON"] = "json";
87
+ AdminForthDataTypes["RICHTEXT"] = "richtext";
87
88
  })(AdminForthDataTypes || (AdminForthDataTypes = {}));
88
89
  export var AdminForthFilterOperators;
89
90
  (function (AdminForthFilterOperators) {
@@ -462,6 +462,7 @@ class CodeInjector implements ICodeInjector {
462
462
 
463
463
  // for every installed plugin generate packages
464
464
  for (const plugin of this.adminforth.activatedPlugins) {
465
+ console.log('🔧 Checking packages for plugin', plugin.customFolderPath);
465
466
  const [lockHash, packages] = await this.packagesFromNpm(plugin.customFolderPath);
466
467
  if (packages.length) {
467
468
  pluginPackages.push({
@@ -449,5 +449,10 @@ export default class ConfigValidator implements IConfigValidator {
449
449
  }
450
450
  })
451
451
  resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
452
+ (resource.plugins || []).forEach((plugin) => {
453
+ if (plugin.validateConfigAfterDiscover) {
454
+ plugin.validateConfigAfterDiscover(this.adminforth, resource);
455
+ }
456
+ });
452
457
  }
453
458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.2.22",
3
+ "version": "1.2.24",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,7 @@
16
16
  "flowbite": "^2.3.0",
17
17
  "flowbite-datepicker": "^1.2.6",
18
18
  "pinia": "^2.1.7",
19
+ "sanitize-html": "^2.13.0",
19
20
  "unhead": "^1.9.12",
20
21
  "uuid": "^10.0.0",
21
22
  "vue": "^3.4.21",
@@ -1897,6 +1898,14 @@
1897
1898
  "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1898
1899
  "dev": true
1899
1900
  },
1901
+ "node_modules/deepmerge": {
1902
+ "version": "4.3.1",
1903
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
1904
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
1905
+ "engines": {
1906
+ "node": ">=0.10.0"
1907
+ }
1908
+ },
1900
1909
  "node_modules/didyoumean": {
1901
1910
  "version": "1.2.2",
1902
1911
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -1938,6 +1947,57 @@
1938
1947
  "node": ">=6.0.0"
1939
1948
  }
1940
1949
  },
1950
+ "node_modules/dom-serializer": {
1951
+ "version": "2.0.0",
1952
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
1953
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
1954
+ "dependencies": {
1955
+ "domelementtype": "^2.3.0",
1956
+ "domhandler": "^5.0.2",
1957
+ "entities": "^4.2.0"
1958
+ },
1959
+ "funding": {
1960
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
1961
+ }
1962
+ },
1963
+ "node_modules/domelementtype": {
1964
+ "version": "2.3.0",
1965
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
1966
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
1967
+ "funding": [
1968
+ {
1969
+ "type": "github",
1970
+ "url": "https://github.com/sponsors/fb55"
1971
+ }
1972
+ ]
1973
+ },
1974
+ "node_modules/domhandler": {
1975
+ "version": "5.0.3",
1976
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
1977
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
1978
+ "dependencies": {
1979
+ "domelementtype": "^2.3.0"
1980
+ },
1981
+ "engines": {
1982
+ "node": ">= 4"
1983
+ },
1984
+ "funding": {
1985
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
1986
+ }
1987
+ },
1988
+ "node_modules/domutils": {
1989
+ "version": "3.1.0",
1990
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
1991
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
1992
+ "dependencies": {
1993
+ "dom-serializer": "^2.0.0",
1994
+ "domelementtype": "^2.3.0",
1995
+ "domhandler": "^5.0.3"
1996
+ },
1997
+ "funding": {
1998
+ "url": "https://github.com/fb55/domutils?sponsor=1"
1999
+ }
2000
+ },
1941
2001
  "node_modules/eastasianwidth": {
1942
2002
  "version": "0.2.0",
1943
2003
  "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2018,7 +2078,6 @@
2018
2078
  "version": "4.0.0",
2019
2079
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
2020
2080
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
2021
- "dev": true,
2022
2081
  "engines": {
2023
2082
  "node": ">=10"
2024
2083
  },
@@ -2545,6 +2604,24 @@
2545
2604
  "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
2546
2605
  "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
2547
2606
  },
2607
+ "node_modules/htmlparser2": {
2608
+ "version": "8.0.2",
2609
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
2610
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
2611
+ "funding": [
2612
+ "https://github.com/fb55/htmlparser2?sponsor=1",
2613
+ {
2614
+ "type": "github",
2615
+ "url": "https://github.com/sponsors/fb55"
2616
+ }
2617
+ ],
2618
+ "dependencies": {
2619
+ "domelementtype": "^2.3.0",
2620
+ "domhandler": "^5.0.3",
2621
+ "domutils": "^3.0.1",
2622
+ "entities": "^4.4.0"
2623
+ }
2624
+ },
2548
2625
  "node_modules/ignore": {
2549
2626
  "version": "5.3.1",
2550
2627
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2673,6 +2750,14 @@
2673
2750
  "node": ">=8"
2674
2751
  }
2675
2752
  },
2753
+ "node_modules/is-plain-object": {
2754
+ "version": "5.0.0",
2755
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
2756
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
2757
+ "engines": {
2758
+ "node": ">=0.10.0"
2759
+ }
2760
+ },
2676
2761
  "node_modules/isexe": {
2677
2762
  "version": "2.0.0",
2678
2763
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3103,6 +3188,11 @@
3103
3188
  "node": ">=6"
3104
3189
  }
3105
3190
  },
3191
+ "node_modules/parse-srcset": {
3192
+ "version": "1.0.2",
3193
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
3194
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
3195
+ },
3106
3196
  "node_modules/path-browserify": {
3107
3197
  "version": "1.0.1",
3108
3198
  "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -3593,6 +3683,19 @@
3593
3683
  "queue-microtask": "^1.2.2"
3594
3684
  }
3595
3685
  },
3686
+ "node_modules/sanitize-html": {
3687
+ "version": "2.13.0",
3688
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz",
3689
+ "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==",
3690
+ "dependencies": {
3691
+ "deepmerge": "^4.2.2",
3692
+ "escape-string-regexp": "^4.0.0",
3693
+ "htmlparser2": "^8.0.0",
3694
+ "is-plain-object": "^5.0.0",
3695
+ "parse-srcset": "^1.0.2",
3696
+ "postcss": "^8.3.11"
3697
+ }
3698
+ },
3596
3699
  "node_modules/sass": {
3597
3700
  "version": "1.77.2",
3598
3701
  "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
package/spa/package.json CHANGED
@@ -20,6 +20,7 @@
20
20
  "flowbite": "^2.3.0",
21
21
  "flowbite-datepicker": "^1.2.6",
22
22
  "pinia": "^2.1.7",
23
+ "sanitize-html": "^2.13.0",
23
24
  "unhead": "^1.9.12",
24
25
  "uuid": "^10.0.0",
25
26
  "vue": "^3.4.21",
@@ -189,8 +189,6 @@ function setFilterItem({ column, operator, value }) {
189
189
  if (index !== -1) {
190
190
  filtersStore.filters.splice(index, 1);
191
191
  }
192
- emits('update:filters', [...filtersStore.filters]);
193
- return;
194
192
  } else {
195
193
  if (index === -1) {
196
194
  filtersStore.setFilter({ field: column.name, value, operator });
@@ -207,5 +205,6 @@ function getFilterItem({ column, operator }) {
207
205
 
208
206
  async function clear() {
209
207
  filtersStore.clearFilters();
208
+ emits('update:filters', [...filtersStore.filters]);
210
209
  }
211
210
  </script>
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <div class="rounded-default">
3
-
4
3
  <div
5
4
  class="relative shadow-resourseFormShadow sm:rounded-lg dark:shadow-2xl dark:shadow-black rounded-default"
6
5
  >
@@ -44,6 +43,7 @@
44
43
  :meta="column.components[props.source].meta"
45
44
  :record="props.record"
46
45
  @update:inValidity="customComponentsInValidity[column.name] = $event"
46
+ @update:emptiness="customComponentsEmptiness[column.name] = $event"
47
47
  />
48
48
  </template>
49
49
  <template v-else>
@@ -95,7 +95,7 @@
95
95
  @input="setCurrentValue(column.name, $event.target.value)"
96
96
  />
97
97
  <textarea
98
- v-else-if="['text'].includes(column.type)"
98
+ v-else-if="['text', 'richtext'].includes(column.type)"
99
99
  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"
100
100
  placeholder="Text"
101
101
  :value="currentValues[column.name]"
@@ -171,6 +171,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
171
171
  const currentValues = ref({});
172
172
 
173
173
  const customComponentsInValidity = ref({});
174
+ const customComponentsEmptiness = ref({});
174
175
 
175
176
 
176
177
  const columnError = (column) => {
@@ -179,7 +180,14 @@ const columnError = (column) => {
179
180
  return customComponentsInValidity.value[column.name];
180
181
  }
181
182
 
182
- if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
183
+ if (
184
+ column.required[mode.value] &&
185
+ (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
186
+ // if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
187
+ // components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
188
+ (customComponentsEmptiness.value[column.name] !== false)
189
+
190
+ ) {
183
191
  return 'This field is required';
184
192
  }
185
193
  if ( column.type === 'string' || column.type === 'text' ) {
@@ -20,6 +20,9 @@
20
20
  <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
21
21
  {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
22
22
  </span>
23
+ <span v-else-if="column.type === 'richtext'">
24
+ <div v-html="protectAgainstXSS(record[column.name])"></div>
25
+ </span>
23
26
  <span v-else>
24
27
  {{ checkEmptyValues(record[column.name],route.meta.type) }}
25
28
  </span>
@@ -34,6 +37,7 @@ import utc from 'dayjs/plugin/utc';
34
37
  import timezone from 'dayjs/plugin/timezone';
35
38
  import {checkEmptyValues} from '@/utils';
36
39
  import { useRoute, useRouter } from 'vue-router';
40
+ import sanitizeHtml from 'sanitize-html';
37
41
 
38
42
 
39
43
  import { useCoreStore } from '@/stores/core';
@@ -50,6 +54,10 @@ const props = defineProps({
50
54
  record: Object
51
55
  });
52
56
 
57
+ function protectAgainstXSS(value) {
58
+ return sanitizeHtml(value);
59
+ }
60
+
53
61
 
54
62
  function formatDate(date) {
55
63
  if (!date) return '';
@@ -285,6 +285,12 @@ export interface IAdminForthPlugin {
285
285
  modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
286
286
  componentPath(componentFile: string): string;
287
287
 
288
+ /**
289
+ * Optional method which will be called after AdminForth discovers all resources and their columns.
290
+ * Can be used to validate types of columns, check if some columns are missing, etc.
291
+ */
292
+ validateConfigAfterDiscover?(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
293
+
288
294
  /**
289
295
  * Here you can register custom endpoints for your plugin.
290
296
  *
@@ -1385,7 +1391,11 @@ export type AdminForthFieldComponents = {
1385
1391
  /**
1386
1392
  * List component is used to redefine cell which renders field value in list view.
1387
1393
  * Component accepts next properties: [record, column, resource, adminUser].
1388
- *
1394
+ * Component can emit events:
1395
+ * - `update:value` - to update record value.
1396
+ * - `update:inValidity` - emit true once entered value became not valid (e.g. emit('update:inValidity', true) ). Emit false once entered value became valid. Emit default value in mounted hook.
1397
+ * - `update:emptiness` - emit true once entered value became empty (e.g. emit('update:emptiness', true) ). Emit false once entered value became not empty. Emit default value in mounted hook.
1398
+ * emptiness emit is optional and required for complex cases. For example for virtual columns where initial value is not set.
1389
1399
  */
1390
1400
  list?: AdminForthComponentDeclaration,
1391
1401
  }
@@ -1401,6 +1411,7 @@ export enum AdminForthDataTypes {
1401
1411
  TIME = 'time',
1402
1412
  TEXT = 'text',
1403
1413
  JSON = 'json',
1414
+ RICHTEXT = 'richtext',
1404
1415
  }
1405
1416
 
1406
1417
  export enum AdminForthFilterOperators {