adminforth 1.0.80 → 1.0.82

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.
Files changed (38) hide show
  1. package/dist/index.js +13 -0
  2. package/dist/modules/codeInjector.js +8 -6
  3. package/dist/spa/spa/src/App.vue +1 -1
  4. package/dist/spa/spa/src/components/ResourceForm.vue +5 -4
  5. package/dist/spa/spa/src/stores/core.ts +1 -0
  6. package/dist/spa/spa/src/utils.ts +12 -0
  7. package/dist/spa/spa/src/views/CreateView.vue +5 -1
  8. package/dist/spa/spa/src/views/EditView.vue +3 -1
  9. package/dist/spa/spa/src/views/ListView.vue +2 -1
  10. package/dist/spa/spa/src/views/ShowView.vue +47 -3
  11. package/documentation/docusaurus.config.ts +34 -26
  12. package/documentation/package.json +1 -1
  13. package/documentation/src/components/HomepageFeatures/index.tsx +13 -15
  14. package/documentation/src/pages/index.module.css +1 -0
  15. package/documentation/src/pages/index.tsx +2 -2
  16. package/documentation/static/img/css.png +0 -0
  17. package/documentation/static/img/db.png +0 -0
  18. package/documentation/static/img/db2.png +0 -0
  19. package/documentation/static/img/gen.sh +24 -0
  20. package/documentation/static/img/tail.png +0 -0
  21. package/documentation/static/img/tail.png:Zone.Identifier +0 -0
  22. package/documentation/static/img/vue.png +0 -0
  23. package/index.ts +13 -0
  24. package/modules/codeInjector.ts +8 -6
  25. package/package.json +2 -2
  26. package/spa/src/App.vue +1 -1
  27. package/spa/src/components/ResourceForm.vue +5 -4
  28. package/spa/src/stores/core.ts +1 -0
  29. package/spa/src/utils.ts +12 -0
  30. package/spa/src/views/CreateView.vue +5 -1
  31. package/spa/src/views/EditView.vue +3 -1
  32. package/spa/src/views/ListView.vue +2 -1
  33. package/spa/src/views/ShowView.vue +47 -3
  34. package/styles.js +52 -0
  35. package/types/AdminForthConfig.ts +240 -9
  36. package/documentation/static/img/undraw_docusaurus_mountain.svg +0 -171
  37. package/documentation/static/img/undraw_docusaurus_react.svg +0 -170
  38. package/documentation/static/img/undraw_docusaurus_tree.svg +0 -40
package/dist/index.js CHANGED
@@ -76,6 +76,13 @@ class AdminForth {
76
76
  if (!this.config.customization.customComponentsDir) {
77
77
  this.config.customization.customComponentsDir = './custom';
78
78
  }
79
+ try {
80
+ // check customComponentsDir exists
81
+ fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
82
+ }
83
+ catch (e) {
84
+ this.config.customization.customComponentsDir = undefined;
85
+ }
79
86
  if (this.config.auth) {
80
87
  if (!this.config.auth.resourceId) {
81
88
  throw new Error('No config.auth.resourceId defined');
@@ -852,6 +859,12 @@ class AdminForth {
852
859
  if (!resource) {
853
860
  return { error: `Resource '${body['resourceId']}' not found` };
854
861
  }
862
+ if (!record) {
863
+ return { error: `Record with ${body['primaryKey']} not found` };
864
+ }
865
+ if (resource.options.allowedActions.delete === false) {
866
+ return { error: `Resource '${resource.resourceId}' does not allow delete action` };
867
+ }
855
868
  // execute hook if needed
856
869
  for (const hook of getFunctionList((_10 = (_9 = resource.hooks) === null || _9 === void 0 ? void 0 : _9.delete) === null || _10 === void 0 ? void 0 : _10.beforeSave)) {
857
870
  const resp = yield hook({ resource, record, adminUser });
@@ -17,6 +17,7 @@ import path from 'path';
17
17
  import { promisify } from 'util';
18
18
  import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
19
19
  import { getComponentNameFromPath } from './utils.js';
20
+ import { styles } from '../styles.js';
20
21
  let TMP_DIR;
21
22
  try {
22
23
  TMP_DIR = os.tmpdir();
@@ -290,6 +291,12 @@ class CodeInjector {
290
291
  tailwindConfigContent = tailwindConfigContent.replace('/* IMPORTANT:ADMINFORTH TAILWIND STYLES */', stylesText);
291
292
  yield fs.promises.writeFile(tailwindConfigPath, tailwindConfigContent);
292
293
  }
294
+ else {
295
+ let tailwindConfigPath = path.join(CodeInjector.SPA_TMP_PATH, 'tailwind.config.js');
296
+ let tailwindConfigContent = yield fs.promises.readFile(tailwindConfigPath, 'utf-8');
297
+ tailwindConfigContent = tailwindConfigContent.replace('/* IMPORTANT:ADMINFORTH TAILWIND STYLES */', styles());
298
+ yield fs.promises.writeFile(tailwindConfigPath, tailwindConfigContent);
299
+ }
293
300
  const routerVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'router', 'index.ts');
294
301
  let routerVueContent = yield fs.promises.readFile(routerVuePath, 'utf-8');
295
302
  routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES IMPORTS */', routerComponents);
@@ -394,12 +401,7 @@ class CodeInjector {
394
401
  watchCustomComponentsForCopy(_a) {
395
402
  return __awaiter(this, arguments, void 0, function* ({ verbose }) {
396
403
  const customComponentsDir = this.adminforth.config.customization.customComponentsDir;
397
- // check dir exists
398
- try {
399
- yield fs.promises.access(customComponentsDir, fs.constants.F_OK);
400
- }
401
- catch (e) {
402
- console.log(`Custom components dir "${customComponentsDir}" does not exist, feel free to create it place custom assets and Vue files there`);
404
+ if (!customComponentsDir) {
403
405
  return;
404
406
  }
405
407
  // check if folder exists
@@ -63,7 +63,7 @@
63
63
  :class="{ '-translate-x-full': !sideBarOpen, 'transform-none': sideBarOpen }"
64
64
  aria-label="Sidebar"
65
65
  >
66
- <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
66
+ <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800 border-r">
67
67
 
68
68
  <div class="flex ms-2 md:me-24 m-4 ">
69
69
  <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="h-8 me-3" />
@@ -121,9 +121,6 @@
121
121
  <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
122
122
  <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
123
123
  </button>
124
-
125
-
126
-
127
124
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
128
125
 
129
126
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
@@ -197,7 +194,11 @@ const columnError = (column) => {
197
194
  for (let i = 0; i < validationArray.length; i++) {
198
195
  if (validationArray[i].regExp) {
199
196
  const regExp = new RegExp(validationArray[i].regExp);
200
- if (!regExp.test(currentValues.value[column.name])) {
197
+ let value = currentValues.value[column.name];
198
+ if (value === undefined || value === null) {
199
+ value = '';
200
+ }
201
+ if (!regExp.test(value)) {
201
202
  return validationArray[i].message;
202
203
  }
203
204
  }
@@ -85,6 +85,7 @@ export const useCoreStore = defineStore('core', () => {
85
85
  } else {
86
86
  resourceColumns.value = res.resource.columns;
87
87
  resourceById.value[resourceId] = res.resource;
88
+ resourceOptions.value = res.resource.options;
88
89
  }
89
90
  }
90
91
 
@@ -2,6 +2,7 @@ import { onMounted, ref, resolveComponent } from 'vue';
2
2
  import type { CoreConfig } from './spa_types/core';
3
3
 
4
4
  import router from "./router";
5
+ import { useRouter } from 'vue-router';
5
6
  import { useCoreStore } from './stores/core';
6
7
 
7
8
  export async function callApi({path, method, body=undefined} ) {
@@ -80,3 +81,14 @@ export function checkEmptyValues(value: any, viewType:'show' | 'list' ) {
80
81
  }
81
82
  return value;
82
83
  }
84
+
85
+ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
86
+ if (!allowedActions) {
87
+ console.warn('allowedActions not set');
88
+ return;
89
+ }
90
+ if(allowedActions[action] === false) {
91
+ console.warn(`Action ${action} is not allowed`);
92
+ router.back();
93
+ }
94
+ }
@@ -23,6 +23,7 @@
23
23
  </BreadcrumbsWithButtons>
24
24
 
25
25
  <SingleSkeletLoader v-if="loading"></SingleSkeletLoader>
26
+
26
27
 
27
28
  <ResourceForm
28
29
  v-else
@@ -35,6 +36,7 @@
35
36
  >
36
37
  </ResourceForm>
37
38
 
39
+
38
40
  </div>
39
41
  </template>
40
42
 
@@ -45,11 +47,12 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
45
47
  import ResourceForm from '@/components/ResourceForm.vue';
46
48
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
47
49
  import { useCoreStore } from '@/stores/core';
48
- import { callAdminForthApi, getCustomComponent } from '@/utils';
50
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
49
51
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
50
52
  import { onMounted, ref } from 'vue';
51
53
  import { useRoute, useRouter } from 'vue-router';
52
54
 
55
+
53
56
  const isValid = ref(false);
54
57
  const validating = ref(false);
55
58
 
@@ -81,6 +84,7 @@ onMounted(async () => {
81
84
  return acc;
82
85
  }, {});
83
86
  loading.value = false;
87
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'create');
84
88
  });
85
89
 
86
90
  async function saveRecord() {
@@ -41,7 +41,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
41
41
  import ResourceForm from '@/components/ResourceForm.vue';
42
42
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
43
43
  import { useCoreStore } from '@/stores/core';
44
- import { callAdminForthApi, getCustomComponent } from '@/utils';
44
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
45
45
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
46
46
  import { computed, onMounted, ref } from 'vue';
47
47
  import { useRoute, useRouter } from 'vue-router';
@@ -79,6 +79,7 @@ const editableRecord = computed(() => {
79
79
  })
80
80
 
81
81
  onMounted(async () => {
82
+
82
83
  loading.value = true;
83
84
 
84
85
  await coreStore.fetchResourceFull({
@@ -88,6 +89,7 @@ onMounted(async () => {
88
89
  resourceId: route.params.resourceId,
89
90
  primaryKey: route.params.primaryKey,
90
91
  });
92
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'edit');
91
93
 
92
94
  editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
93
95
  if (column.component?.edit) {
@@ -65,7 +65,7 @@
65
65
  </BreadcrumbsWithButtons>
66
66
 
67
67
  <!-- table -->
68
- <div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black rounded-default">
68
+ <div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black overflow-y-hidden rounded-default">
69
69
 
70
70
 
71
71
  <!-- skelet loader -->
@@ -208,6 +208,7 @@
208
208
  <td class=" items-center px-6 py-4">
209
209
  <div class="flex">
210
210
  <RouterLink
211
+ v-if="allowedActions.show"
211
212
  :to="{ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
212
213
  class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
213
214
  :data-tooltip-target="`tooltip-show-${rowI}`"
@@ -8,7 +8,7 @@
8
8
  Edit
9
9
  </RouterLink>
10
10
 
11
- <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="deleteRecord"
11
+ <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="showDeleteModal"
12
12
  class="flex items-center py-1 px-3 mb-2 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
13
13
  >
14
14
  <IconTrashBinSolid class="w-4 h-4" />
@@ -83,20 +83,26 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
83
83
 
84
84
  import ValueRenderer from '@/components/ValueRenderer.vue';
85
85
  import { useCoreStore } from '@/stores/core';
86
- import { getCustomComponent } from '@/utils';
86
+ import { useModalStore } from '@/stores/modal';
87
+ import { getCustomComponent,checkAcessByAllowedActions } from '@/utils';
87
88
  import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
88
89
  import { onMounted, ref } from 'vue';
89
- import { useRoute } from 'vue-router';
90
+ import { useRoute,useRouter } from 'vue-router';
91
+ import {callAdminForthApi} from '@/utils';
90
92
 
91
93
 
92
94
  const item = ref(null);
93
95
  const route = useRoute();
96
+ const router = useRouter();
94
97
  const loading = ref(false);
95
98
  let showComponentsPerColumn = {};
96
99
  let showRowComponentsPerColumn = {};
97
100
 
101
+ console.log(route.params,'showWiev');
102
+
98
103
 
99
104
  const coreStore = useCoreStore();
105
+ const modalStore = useModalStore();
100
106
 
101
107
  onMounted(async () => {
102
108
  loading.value = true;
@@ -107,6 +113,8 @@ onMounted(async () => {
107
113
  resourceId: route.params.resourceId,
108
114
  primaryKey: route.params.primaryKey,
109
115
  });
116
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'show');
117
+
110
118
  showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
111
119
  if (column.component?.show) {
112
120
  acc[column.name] = getCustomComponent(column.component.show);
@@ -123,4 +131,40 @@ onMounted(async () => {
123
131
  loading.value = false;
124
132
  });
125
133
 
134
+ function showDeleteModal(row) {
135
+ if (!coreStore.config?.deleteConfirmation) {
136
+ return deleteRecord(row);
137
+ }
138
+ modalStore.setModalContent({
139
+ content: 'Are you sure you want to delete this item?',
140
+ acceptText: 'Delete',
141
+ cancelText: 'Cancel',
142
+ });
143
+ modalStore.setOnAcceptFunction(() => {
144
+ return deleteRecord(row)
145
+ })
146
+ modalStore.togleModal();
147
+
148
+
149
+ }
150
+
151
+ async function deleteRecord(row) {
152
+ try{
153
+ await callAdminForthApi({
154
+ path: '/delete_record',
155
+ method: 'POST',
156
+ body: {
157
+ resourceId: route.params.resourceId,
158
+ primaryKey: route.params.primaryKey,
159
+ recordId: route.params.primaryKey
160
+ }
161
+ });
162
+ router.push({ name: 'resource-list', params: { resourceId: route.params.resourceId } });
163
+ } catch (error) {
164
+ console.log(error);
165
+ }
166
+
167
+ modalStore.resetmodalState()
168
+ }
169
+
126
170
  </script>
@@ -4,7 +4,7 @@ import type * as Preset from '@docusaurus/preset-classic';
4
4
 
5
5
  const config: Config = {
6
6
  title: 'AdminForth',
7
- tagline: 'AdminForth Open-Source Vue-driven Next-Gen Admin Panel',
7
+ tagline: 'OpenSource Dashboard on Tailwind UI extendable with Vue3 ',
8
8
  favicon: 'img/favicon.png',
9
9
 
10
10
  // Set the production url of your site here
@@ -16,7 +16,7 @@ const config: Config = {
16
16
  // GitHub pages deployment config.
17
17
  // If you aren't using GitHub pages, you don't need these.
18
18
  organizationName: 'devforth', // Usually your GitHub org/user name.
19
- projectName: 'adminforth', // Usually your repo name.
19
+ projectName: 'devforth.github.io', // Usually your repo name.
20
20
 
21
21
  onBrokenLinks: 'throw',
22
22
  onBrokenMarkdownLinks: 'warn',
@@ -58,7 +58,7 @@ const config: Config = {
58
58
  [
59
59
  "docusaurus-plugin-typedoc",
60
60
  {
61
- entryPoints: "../index.ts",
61
+ entryPoints: ["../types/AdminForthConfig.ts"],
62
62
  plugin: ["./typedoc-plugin.mjs"],
63
63
  readme: "none",
64
64
  indexFormat: "table",
@@ -96,7 +96,7 @@ const config: Config = {
96
96
  position: 'left',
97
97
  label: 'API',
98
98
  },
99
- {to: '/blog', label: 'Blog', position: 'left'},
99
+ // {to: '/blog', label: 'Blog', position: 'left'},
100
100
  {
101
101
  href: 'https://github.com/devforth/adminforth',
102
102
  label: 'GitHub',
@@ -112,45 +112,53 @@ const config: Config = {
112
112
  items: [
113
113
  {
114
114
  label: 'Tutorial',
115
- to: '/docs/intro',
115
+ to: '/docs/Getting%20Started',
116
116
  },
117
117
  ],
118
118
  },
119
+ // {
120
+ // title: 'Community',
121
+ // items: [
122
+ // {
123
+ // label: 'Stack Overflow',
124
+ // href: 'https://stackoverflow.com/questions/tagged/docusaurus',
125
+ // },
126
+ // {
127
+ // label: 'Discord',
128
+ // href: 'https://discordapp.com/invite/docusaurus',
129
+ // },
130
+ // {
131
+ // label: 'Twitter',
132
+ // href: 'https://twitter.com/docusaurus',
133
+ // },
134
+ // ],
135
+ // },
119
136
  {
120
- title: 'Community',
137
+ title: 'More',
121
138
  items: [
139
+ // {
140
+ // label: 'Blog',
141
+ // to: '/blog',
142
+ // },
122
143
  {
123
- label: 'Stack Overflow',
124
- href: 'https://stackoverflow.com/questions/tagged/docusaurus',
125
- },
126
- {
127
- label: 'Discord',
128
- href: 'https://discordapp.com/invite/docusaurus',
129
- },
130
- {
131
- label: 'Twitter',
132
- href: 'https://twitter.com/docusaurus',
144
+ label: 'DevForth.io',
145
+ href: 'https://devforth.io',
133
146
  },
134
- ],
135
- },
136
- {
137
- title: 'More',
138
- items: [
139
147
  {
140
- label: 'Blog',
141
- to: '/blog',
148
+ label: 'We can develop dashboard for you',
149
+ href: 'https://devforth.io/contact',
142
150
  },
143
151
  {
144
152
  label: 'GitHub',
145
- href: 'https://github.com/facebook/docusaurus',
153
+ href: 'https://github.com/devforth/adminforth',
146
154
  },
147
155
  ],
148
156
  },
149
157
  ],
150
- copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
158
+ copyright: `Copyright © ${new Date().getFullYear()} Devforth sp. z o.o.`,
151
159
  },
152
160
  prism: {
153
- theme: prismThemes.github,
161
+ theme: prismThemes.vsLight,
154
162
  darkTheme: prismThemes.dracula,
155
163
  },
156
164
  } satisfies Preset.ThemeConfig,
@@ -8,7 +8,7 @@
8
8
  "start": "docusaurus start",
9
9
  "build": "docusaurus build",
10
10
  "swizzle": "docusaurus swizzle",
11
- "deploy": "docusaurus deploy",
11
+ "deploy": "DEPLOYMENT_BRANCH=main docusaurus deploy",
12
12
  "clear": "docusaurus clear",
13
13
  "serve": "docusaurus serve",
14
14
  "write-translations": "docusaurus write-translations",
@@ -4,48 +4,46 @@ import styles from './styles.module.css';
4
4
 
5
5
  type FeatureItem = {
6
6
  title: string;
7
- Svg: React.ComponentType<React.ComponentProps<'svg'>>;
7
+ img: string;
8
8
  description: JSX.Element;
9
9
  };
10
10
 
11
11
  const FeatureList: FeatureItem[] = [
12
12
  {
13
- title: 'Easy to Use',
14
- Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
13
+ title: 'CRUD Out of the Box',
14
+ img: require('@site/static/img/db2.png').default,
15
15
  description: (
16
16
  <>
17
- Docusaurus was designed from the ground up to be easily installed and
18
- used to get your website up and running quickly.
17
+ Initialize AdminForth with your database URL and get a full-fledged admin panel to
18
+ List, Create, Update, Filter and Delete data with ease.
19
19
  </>
20
20
  ),
21
21
  },
22
22
  {
23
- title: 'Focus on What Matters',
24
- Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
23
+ title: 'Vue3 Driven',
24
+ img: require('@site/static/img/vue.png').default,
25
25
  description: (
26
26
  <>
27
- Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
28
- ahead and move your docs into the <code>docs</code> directory.
27
+ Extend easily by creating own Vue3 components and pages
29
28
  </>
30
29
  ),
31
30
  },
32
31
  {
33
- title: 'Powered by React',
34
- Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
32
+ title: 'Tailwind Look',
33
+ img: require('@site/static/img/css.png').default,
35
34
  description: (
36
35
  <>
37
- Extend or customize your website layout by reusing React. Docusaurus can
38
- be extended while reusing the same header and footer.
36
+ Look is based on TailwindCSS, with a themes customization and dark mode available
39
37
  </>
40
38
  ),
41
39
  },
42
40
  ];
43
41
 
44
- function Feature({title, Svg, description}: FeatureItem) {
42
+ function Feature({title, img, description}: FeatureItem) {
45
43
  return (
46
44
  <div className={clsx('col col--4')}>
47
45
  <div className="text--center">
48
- <Svg className={styles.featureSvg} role="img" />
46
+ <img src={img} alt={title} style={{borderRadius: '50px', width: 250}} />
49
47
  </div>
50
48
  <div className="text--center padding-horiz--md">
51
49
  <Heading as="h3">{title}</Heading>
@@ -8,6 +8,7 @@
8
8
  text-align: center;
9
9
  position: relative;
10
10
  overflow: hidden;
11
+ background-color: #48C5FF;
11
12
  }
12
13
 
13
14
  @media screen and (max-width: 996px) {
@@ -19,8 +19,8 @@ function HomepageHeader() {
19
19
  <div className={styles.buttons}>
20
20
  <Link
21
21
  className="button button--secondary button--lg"
22
- to="/docs/intro">
23
- Docusaurus Tutorial - 5min ⏱️
22
+ to="/docs/Getting%20Started">
23
+ AdminForth Tutorial - 5min ⏱️
24
24
  </Link>
25
25
  </div>
26
26
  </div>
Binary file
Binary file
Binary file
@@ -0,0 +1,24 @@
1
+ DO_N_TIME=10
2
+
3
+ for i in $(seq 1 $DO_N_TIME)
4
+ do
5
+ # curl https://api.openai.com/v1/images/generations \
6
+ # -H "Content-Type: application/json" \
7
+ # -H "Authorization: Bearer $OPENAI_TOKEN" \
8
+ # -d '{
9
+ # "model": "dall-e-3",
10
+ # "prompt": "A photorealistic image of a cute kitty lying on top of a database symbol. Kitty should have eyes open. The database should be represented as a cylindrical shape, and the kitty should look relaxed and content, possibly curled up or stretched out.",
11
+ # "n": 1,
12
+ # "size": "1024x1024"
13
+ # }'
14
+
15
+ curl https://api.openai.com/v1/images/generations \
16
+ -H "Content-Type: application/json" \
17
+ -H "Authorization: Bearer $OPENAI_TOKEN" \
18
+ -d '{
19
+ "model": "dall-e-3",
20
+ "prompt": "A photorealistic image of a cute kitty playing with nice rectangle box. Box is rounded and has CSS on it. The kitty should look playful and curious",
21
+ "n": 1,
22
+ "size": "1024x1024"
23
+ }'
24
+ done
Binary file
package/index.ts CHANGED
@@ -98,6 +98,13 @@ class AdminForth implements AdminForthClass {
98
98
  this.config.customization.customComponentsDir = './custom';
99
99
  }
100
100
 
101
+ try {
102
+ // check customComponentsDir exists
103
+ fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
104
+ } catch (e) {
105
+ this.config.customization.customComponentsDir = undefined;
106
+ }
107
+
101
108
  if (this.config.auth) {
102
109
  if (!this.config.auth.resourceId) {
103
110
  throw new Error('No config.auth.resourceId defined');
@@ -959,6 +966,12 @@ class AdminForth implements AdminForthClass {
959
966
  if (!resource) {
960
967
  return { error: `Resource '${body['resourceId']}' not found` };
961
968
  }
969
+ if (!record){
970
+ return { error: `Record with ${body['primaryKey']} not found` };
971
+ }
972
+ if (resource.options.allowedActions.delete === false) {
973
+ return { error: `Resource '${resource.resourceId}' does not allow delete action` };
974
+ }
962
975
 
963
976
  // execute hook if needed
964
977
  for (const hook of getFunctionList(resource.hooks?.delete?.beforeSave)) {
@@ -9,6 +9,8 @@ import { promisify } from 'util';
9
9
  import AdminForth from '../index.js';
10
10
  import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
11
11
  import { getComponentNameFromPath } from './utils.js';
12
+ import { styles } from '../styles.js'
13
+
12
14
 
13
15
 
14
16
 
@@ -327,6 +329,11 @@ class CodeInjector {
327
329
  let tailwindConfigContent = await fs.promises.readFile(tailwindConfigPath, 'utf-8');
328
330
  tailwindConfigContent = tailwindConfigContent.replace('/* IMPORTANT:ADMINFORTH TAILWIND STYLES */', stylesText);
329
331
  await fs.promises.writeFile(tailwindConfigPath, tailwindConfigContent);
332
+ } else {
333
+ let tailwindConfigPath = path.join(CodeInjector.SPA_TMP_PATH, 'tailwind.config.js');
334
+ let tailwindConfigContent = await fs.promises.readFile(tailwindConfigPath, 'utf-8');
335
+ tailwindConfigContent = tailwindConfigContent.replace('/* IMPORTANT:ADMINFORTH TAILWIND STYLES */',styles());
336
+ await fs.promises.writeFile(tailwindConfigPath, tailwindConfigContent);
330
337
  }
331
338
 
332
339
 
@@ -456,14 +463,9 @@ async watchForReprepare({ verbose }) {
456
463
 
457
464
  async watchCustomComponentsForCopy({ verbose }) {
458
465
  const customComponentsDir = this.adminforth.config.customization.customComponentsDir;
459
- // check dir exists
460
- try {
461
- await fs.promises.access(customComponentsDir, fs.constants.F_OK);
462
- } catch (e) {
463
- console.log(`Custom components dir "${customComponentsDir}" does not exist, feel free to create it place custom assets and Vue files there`);
466
+ if (!customComponentsDir) {
464
467
  return;
465
468
  }
466
-
467
469
  // check if folder exists
468
470
  try {
469
471
  await fs.promises.access(customComponentsDir, fs.constants.F_OK);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "build": "tsc",
10
- "rollout": "tsc && cp -r spa dist/spa && npm version patch && npm publish",
10
+ "rollout": "tsc && cp -r spa dist/spa && npm version patch && npm publish && cd documentation && npm run deploy",
11
11
  "docs": "typedoc"
12
12
  },
13
13
  "author": "devforth.io",
package/spa/src/App.vue CHANGED
@@ -63,7 +63,7 @@
63
63
  :class="{ '-translate-x-full': !sideBarOpen, 'transform-none': sideBarOpen }"
64
64
  aria-label="Sidebar"
65
65
  >
66
- <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800">
66
+ <div class="h-full px-3 pb-4 overflow-y-auto bg-nav-menu-bg dark:bg-gray-800 border-r">
67
67
 
68
68
  <div class="flex ms-2 md:me-24 m-4 ">
69
69
  <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="h-8 me-3" />