adminforth 1.0.83 → 1.0.86

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 (48) hide show
  1. package/dist/index.js +34 -16
  2. package/dist/modules/codeInjector.js +19 -10
  3. package/dist/plugins/ForeignInlineListPlugin/index.js +3 -2
  4. package/dist/servers/express.js +0 -3
  5. package/dist/spa/spa/package-lock.json +13 -0
  6. package/dist/spa/spa/package.json +1 -0
  7. package/dist/spa/spa/src/App.vue +44 -19
  8. package/dist/spa/spa/src/components/AcceptModal.vue +2 -9
  9. package/dist/spa/spa/src/components/Toast.vue +65 -0
  10. package/dist/spa/spa/src/components/ValueRenderer.vue +8 -8
  11. package/dist/spa/spa/src/composables/useStores.ts +51 -0
  12. package/dist/spa/spa/src/stores/core.ts +5 -1
  13. package/dist/spa/spa/src/stores/modal.ts +13 -2
  14. package/dist/spa/spa/src/stores/toast.ts +15 -0
  15. package/dist/spa/spa/src/views/CreateView.vue +25 -2
  16. package/dist/spa/spa/src/views/EditView.vue +28 -3
  17. package/dist/spa/spa/src/views/ListView.vue +28 -5
  18. package/dist/spa/spa/src/views/ShowView.vue +33 -7
  19. package/dist/types/AdminForthConfig.js +54 -0
  20. package/dist/types/FrontendAPI.js +7 -0
  21. package/documentation/docs/Getting Started.md +374 -0
  22. package/documentation/docs/Glossary.md +37 -0
  23. package/documentation/docs/image.png +0 -0
  24. package/documentation/docusaurus.config.ts +4 -4
  25. package/documentation/static/CNAME +1 -0
  26. package/index.ts +50 -35
  27. package/modules/codeInjector.ts +15 -6
  28. package/package.json +1 -1
  29. package/plugins/ForeignInlineListPlugin/index.ts +3 -3
  30. package/servers/express.ts +4 -9
  31. package/spa/package-lock.json +13 -0
  32. package/spa/package.json +1 -0
  33. package/spa/src/App.vue +44 -19
  34. package/spa/src/components/AcceptModal.vue +2 -9
  35. package/spa/src/components/Toast.vue +65 -0
  36. package/spa/src/components/ValueRenderer.vue +8 -8
  37. package/spa/src/composables/useStores.ts +51 -0
  38. package/spa/src/stores/core.ts +5 -1
  39. package/spa/src/stores/modal.ts +13 -2
  40. package/spa/src/stores/toast.ts +15 -0
  41. package/spa/src/views/CreateView.vue +25 -2
  42. package/spa/src/views/EditView.vue +28 -3
  43. package/spa/src/views/ListView.vue +28 -5
  44. package/spa/src/views/ShowView.vue +33 -7
  45. package/types/AdminForthConfig.ts +469 -42
  46. package/types/FrontendAPI.ts +73 -0
  47. package/documentation/static/img/docusaurus-social-card.jpg +0 -0
  48. package/documentation/static/img/tail.png:Zone.Identifier +0 -0
package/dist/index.js CHANGED
@@ -23,10 +23,10 @@ import ExpressServer from './servers/express.js';
23
23
  import { v1 as uuid } from 'uuid';
24
24
  import fs from 'fs';
25
25
  import { ADMINFORTH_VERSION } from './modules/utils.js';
26
- import { AdminForthFilterOperators, AdminForthDataTypes } from './types/AdminForthConfig.js';
26
+ import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
27
27
  import { getFunctionList } from './modules/utils.js';
28
28
  import path from 'path';
29
- const AVAILABLE_SHOW_IN = ['list', 'edit', 'create', 'filter', 'show'];
29
+ //get array from enum AdminForthResourcePages
30
30
  const DEFAULT_ALLOWED_ACTIONS = { create: true, edit: true, show: true, delete: true };
31
31
  class AdminForth {
32
32
  constructor(config) {
@@ -122,12 +122,12 @@ class AdminForth {
122
122
  if (!res.table) {
123
123
  errors.push(`Resource "${res.dataSource}" is missing table`);
124
124
  }
125
- // if itemLabel is not callable, throw error
126
- if (res.itemLabel && typeof res.itemLabel !== 'function') {
127
- errors.push(`Resource "${res.dataSource}" itemLabel is not a function`);
125
+ // if recordLabel is not callable, throw error
126
+ if (res.recordLabel && typeof res.recordLabel !== 'function') {
127
+ errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
128
128
  }
129
- if (!res.itemLabel) {
130
- res.itemLabel = (item) => {
129
+ if (!res.recordLabel) {
130
+ res.recordLabel = (item) => {
131
131
  const pkVal = item[res.columns.find((col) => col.primaryKey).name];
132
132
  return `${res.label} ${pkVal}`;
133
133
  };
@@ -141,7 +141,6 @@ class AdminForth {
141
141
  res.columns = [];
142
142
  }
143
143
  res.columns.forEach((col) => {
144
- var _b;
145
144
  col.label = col.label || guessLabelFromName(col.name);
146
145
  //define default sortable
147
146
  if (!Object.keys(col).includes('sortable')) {
@@ -171,11 +170,11 @@ class AdminForth {
171
170
  errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
172
171
  }
173
172
  }
174
- const wrongShowIn = col.showIn && col.showIn.find((c) => !AVAILABLE_SHOW_IN.includes(c));
173
+ const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
175
174
  if (wrongShowIn) {
176
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${AVAILABLE_SHOW_IN.join(', ')}`);
175
+ errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
177
176
  }
178
- col.showIn = ((_b = col.showIn) === null || _b === void 0 ? void 0 : _b.map(c => c.toLowerCase())) || AVAILABLE_SHOW_IN;
177
+ col.showIn = col.showIn || Object.values(AdminForthResourcePages);
179
178
  });
180
179
  if (!res.options) {
181
180
  res.options = { bulkActions: [], allowedActions: {} };
@@ -204,6 +203,25 @@ class AdminForth {
204
203
  return Object.assign(action, { id: uuid() });
205
204
  });
206
205
  bulkActions = newBulkActions;
206
+ // if pageInjection is a string, make array with one element. Also check file exists
207
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
208
+ if (res.options.pageInjections) {
209
+ Object.entries(res.options.pageInjections).map(([key, value]) => {
210
+ Object.entries(value).map(([injection, target]) => {
211
+ if (possibleInjections.includes(injection)) {
212
+ if (typeof target === 'string') {
213
+ res.options.pageInjections[key][injection] = [target];
214
+ }
215
+ res.options.pageInjections[key][injection].forEach((target) => {
216
+ errors.push(...this.checkCustomFileExists(target));
217
+ });
218
+ }
219
+ else {
220
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
221
+ }
222
+ });
223
+ });
224
+ }
207
225
  }
208
226
  //add default allowedActions to resources
209
227
  if (res.options.allowedActions) {
@@ -277,8 +295,8 @@ class AdminForth {
277
295
  // check is all custom components files exists
278
296
  for (const resource of this.config.resources) {
279
297
  for (const column of resource.columns) {
280
- if (column.component) {
281
- for (const [key, value] of Object.entries(column.component)) {
298
+ if (column.components) {
299
+ for (const [key, value] of Object.entries(column.components)) {
282
300
  if (this.codeInjector.allComponentNames[value]) {
283
301
  // not obvious, but if we are in this if, it means that this is plugin component
284
302
  // and there is no sense to check if it exists in users folder
@@ -594,7 +612,7 @@ class AdminForth {
594
612
  });
595
613
  const targetDataMap = targetData.data.reduce((acc, item) => {
596
614
  acc[item[targetResourcePkField]] = {
597
- label: targetResource.itemLabel(item),
615
+ label: targetResource.recordLabel(item),
598
616
  pk: item[targetResourcePkField],
599
617
  };
600
618
  return acc;
@@ -621,7 +639,7 @@ class AdminForth {
621
639
  });
622
640
  });
623
641
  data.data.forEach((item) => {
624
- item._label = resource.itemLabel(item);
642
+ item._label = resource.recordLabel(item);
625
643
  });
626
644
  return Object.assign(Object.assign({}, data), { options: resource === null || resource === void 0 ? void 0 : resource.options });
627
645
  }),
@@ -670,7 +688,7 @@ class AdminForth {
670
688
  });
671
689
  const items = dbDataItems.data.map((item) => {
672
690
  const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
673
- const labler = targetResource.itemLabel;
691
+ const labler = targetResource.recordLabel;
674
692
  return {
675
693
  value: pk,
676
694
  label: labler(item),
@@ -94,7 +94,7 @@ class CodeInjector {
94
94
  }
95
95
  prepareSources(_a) {
96
96
  return __awaiter(this, arguments, void 0, function* ({ filesUpdated, verbose = false }) {
97
- var _b, _c, _d, _e, _f, _g, _h, _j, _k;
97
+ var _b, _c, _d, _e, _f, _g, _h, _j;
98
98
  // check SPA_TMP_PATH exists and create if not
99
99
  try {
100
100
  yield fs.promises.access(CodeInjector.SPA_TMP_PATH, fs.constants.F_OK);
@@ -233,18 +233,27 @@ class CodeInjector {
233
233
  return `import { ${PascalIconName} } from '@iconify-prerendered/vue-${collection}';`;
234
234
  }).join('\n');
235
235
  // for each custom component generate import statement
236
- const customComponentsDir = (_c = this.adminforth.config.customization) === null || _c === void 0 ? void 0 : _c.customComponentsDir;
237
236
  const customResourceComponents = [];
238
237
  this.adminforth.config.resources.forEach((resource) => {
238
+ var _a;
239
239
  resource.columns.forEach((field) => {
240
- if (field.component) {
241
- Object.values(field.component).forEach((filePath) => {
240
+ if (field.components) {
241
+ Object.values(field.components).forEach((filePath) => {
242
242
  if (!customResourceComponents.includes(filePath)) {
243
243
  customResourceComponents.push(filePath);
244
244
  }
245
245
  });
246
246
  }
247
247
  });
248
+ (Object.values(((_a = resource.options) === null || _a === void 0 ? void 0 : _a.pageInjections) || {})).forEach((injection) => {
249
+ Object.values(injection).forEach((filePathes) => {
250
+ filePathes.forEach((filePath) => {
251
+ if (!customResourceComponents.includes(filePath)) {
252
+ customResourceComponents.push(filePath);
253
+ }
254
+ });
255
+ });
256
+ });
248
257
  });
249
258
  customResourceComponents.forEach((filePath) => {
250
259
  const componentName = getComponentNameFromPath(filePath);
@@ -270,7 +279,7 @@ class CodeInjector {
270
279
  }
271
280
  let imports = iconImports + '\n';
272
281
  imports += customComponentsImports + '\n';
273
- if ((_d = this.adminforth.config.customization) === null || _d === void 0 ? void 0 : _d.vueUsesFile) {
282
+ if ((_c = this.adminforth.config.customization) === null || _c === void 0 ? void 0 : _c.vueUsesFile) {
274
283
  imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
275
284
  }
276
285
  // inject that code into spa_tmp/src/App.vue
@@ -278,13 +287,13 @@ class CodeInjector {
278
287
  let appVueContent = yield fs.promises.readFile(appVuePath, 'utf-8');
279
288
  appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
280
289
  appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' + customComponentsComponents + '\n');
281
- if ((_e = this.adminforth.config.customization) === null || _e === void 0 ? void 0 : _e.vueUsesFile) {
290
+ if ((_d = this.adminforth.config.customization) === null || _d === void 0 ? void 0 : _d.vueUsesFile) {
282
291
  appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
283
292
  }
284
293
  yield fs.promises.writeFile(appVuePath, appVueContent);
285
294
  // generate tailwind extend styles
286
- if ((_f = this.adminforth.config) === null || _f === void 0 ? void 0 : _f.styles) {
287
- const tailwindStyles = (_g = this.adminforth.config) === null || _g === void 0 ? void 0 : _g.styles;
295
+ if ((_e = this.adminforth.config) === null || _e === void 0 ? void 0 : _e.styles) {
296
+ const tailwindStyles = (_f = this.adminforth.config) === null || _f === void 0 ? void 0 : _f.styles;
288
297
  const stylesText = JSON.stringify(tailwindStyles, null, 2).slice(1, -1);
289
298
  let tailwindConfigPath = path.join(CodeInjector.SPA_TMP_PATH, 'tailwind.config.js');
290
299
  let tailwindConfigContent = yield fs.promises.readFile(tailwindConfigPath, 'utf-8');
@@ -308,10 +317,10 @@ class CodeInjector {
308
317
  /* generate custom routes */
309
318
  const homepageMenuItem = this.adminforth.config.menu.find((mi) => mi.homepage);
310
319
  let childrenHomePageMenuItem = this.adminforth.config.menu.find((mi) => { var _a; return mi.children && ((_a = mi.children) === null || _a === void 0 ? void 0 : _a.find((mi) => mi.homepage)); });
311
- let childrenHomepage = (_h = childrenHomePageMenuItem === null || childrenHomePageMenuItem === void 0 ? void 0 : childrenHomePageMenuItem.children) === null || _h === void 0 ? void 0 : _h.find((mi) => mi.homepage);
320
+ let childrenHomepage = (_g = childrenHomePageMenuItem === null || childrenHomePageMenuItem === void 0 ? void 0 : childrenHomePageMenuItem.children) === null || _g === void 0 ? void 0 : _g.find((mi) => mi.homepage);
312
321
  let homePagePath = (homepageMenuItem === null || homepageMenuItem === void 0 ? void 0 : homepageMenuItem.path) || `/resource/${childrenHomepage === null || childrenHomepage === void 0 ? void 0 : childrenHomepage.resourceId}`;
313
322
  if (!homePagePath) {
314
- homePagePath = ((_j = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _j === void 0 ? void 0 : _j.path) || `/resource/${(_k = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _k === void 0 ? void 0 : _k.resourceId}`;
323
+ homePagePath = ((_h = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _h === void 0 ? void 0 : _h.path) || `/resource/${(_j = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _j === void 0 ? void 0 : _j.resourceId}`;
315
324
  }
316
325
  routes += `{
317
326
  path: '/',
@@ -1,3 +1,4 @@
1
+ import { AdminForthResourcePages } from "../../types/AdminForthConfig.js";
1
2
  import AdminForthPlugin from "../base.js";
2
3
  export default class ForeignInlineListPlugin extends AdminForthPlugin {
3
4
  constructor(options) {
@@ -15,8 +16,8 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
15
16
  name: `foreignInlineList_${this.foreignResource.resourceId}`,
16
17
  label: 'Foreign Inline List',
17
18
  virtual: true,
18
- showIn: ['show'],
19
- component: {
19
+ showIn: [AdminForthResourcePages.show],
20
+ components: {
20
21
  showRow: this.componentPath('InlineList.vue'),
21
22
  },
22
23
  });
@@ -8,12 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import path from 'path';
11
- import { fileURLToPath } from 'url';
12
11
  import fs from 'fs';
13
12
  import CodeInjector from '../modules/codeInjector.js';
14
13
  import fetch from 'node-fetch';
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = path.dirname(__filename);
17
14
  function replaceAtStart(string, substring) {
18
15
  if (string.startsWith(substring)) {
19
16
  return string.slice(substring.length);
@@ -17,6 +17,7 @@
17
17
  "flowbite-datepicker": "^1.2.6",
18
18
  "pinia": "^2.1.7",
19
19
  "unhead": "^1.9.12",
20
+ "uuid": "^10.0.0",
20
21
  "vue": "^3.4.21",
21
22
  "vue-router": "^4.3.0",
22
23
  "vue-slider-component": "^4.1.0-beta.7"
@@ -4032,6 +4033,18 @@
4032
4033
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
4033
4034
  "dev": true
4034
4035
  },
4036
+ "node_modules/uuid": {
4037
+ "version": "10.0.0",
4038
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
4039
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
4040
+ "funding": [
4041
+ "https://github.com/sponsors/broofa",
4042
+ "https://github.com/sponsors/ctavan"
4043
+ ],
4044
+ "bin": {
4045
+ "uuid": "dist/bin/uuid"
4046
+ }
4047
+ },
4035
4048
  "node_modules/vite": {
4036
4049
  "version": "5.2.13",
4037
4050
  "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz",
@@ -21,6 +21,7 @@
21
21
  "flowbite-datepicker": "^1.2.6",
22
22
  "pinia": "^2.1.7",
23
23
  "unhead": "^1.9.12",
24
+ "uuid": "^10.0.0",
24
25
  "vue": "^3.4.21",
25
26
  "vue-router": "^4.3.0",
26
27
  "vue-slider-component": "^4.1.0-beta.7"
@@ -123,15 +123,43 @@
123
123
  </div>
124
124
  </div>
125
125
  <AcceptModal />
126
+ <div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse">
127
+ <transition-group
128
+ name="fade"
129
+ tag="div"
130
+ class="flex flex-col-reverse gap-1"
131
+ >
132
+ <Toast :toast="t" @close="toastStore.removeToast(t)" v-for="(t,i) in toastStore.toasts" :key="`t-${t.id}`" ></Toast>
133
+ </transition-group>
134
+ </div>
126
135
 
127
136
  <div v-if="sideBarOpen"
128
137
  @click="sideBarOpen = false"
129
138
 
130
139
  drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"></div>
131
140
 
141
+
142
+
132
143
  </template>
133
144
 
134
- <style scoped>
145
+ <style lang="scss" scoped>
146
+
147
+ .fade-leave-active {
148
+ @apply transition-opacity duration-500;
149
+ }
150
+ .fade-leave-to {
151
+ @apply opacity-0;
152
+ }
153
+ .fade-enter-active {
154
+ @apply transition-opacity duration-500;
155
+ }
156
+ .fade-enter-from {
157
+ @apply opacity-0;
158
+ }
159
+ .fade-enter-to {
160
+ @apply opacity-100;
161
+ }
162
+
135
163
  </style>
136
164
 
137
165
  <script setup lang="ts">
@@ -140,6 +168,7 @@ import { RouterLink, RouterView } from 'vue-router';
140
168
  import { initFlowbite } from 'flowbite'
141
169
  import './index.scss'
142
170
  import { useCoreStore } from '@/stores/core';
171
+ import {useModalStore} from '@/stores/modal';
143
172
  import { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
144
173
  import AcceptModal from './components/AcceptModal.vue';
145
174
  import MenuLink from './components/MenuLink.vue';
@@ -148,33 +177,22 @@ import { getIcon } from '@/utils';
148
177
  import { useHead } from 'unhead'
149
178
  import { createHead } from 'unhead'
150
179
  import { loadFile } from './utils';
151
-
180
+ import Toast from './components/Toast.vue';
181
+ import {useToastStore} from '@/stores/toast';
182
+ import { FrontendAPI } from '@/composables/useStores'
152
183
  // import { link } from 'fs';
153
184
  const coreStore = useCoreStore();
154
-
155
-
185
+ const modalStore = useModalStore();
186
+ const toastStore = useToastStore();
187
+ const frontendApi = new FrontendAPI();
188
+ frontendApi.init();
156
189
  const splitAtLast = (str: string, separator: string) => {
157
190
  const index = str.lastIndexOf(separator);
158
191
  return [str.slice(0, index), str.slice(index + 1)];
159
192
  }
160
-
161
-
162
-
163
193
  createHead()
164
-
165
-
166
194
  const sideBarOpen = ref(false);
167
-
168
-
169
-
170
195
  const route = useRoute();
171
- watch(route, () => {
172
- title.value = `${coreStore.config?.title || coreStore.config?.brandName || 'Adminforth'} | ${ Object.values(route.params)[0] || route.meta.title || ' '}`;
173
- useHead({
174
- title: title.value,
175
- })
176
- });
177
-
178
196
  const router = useRouter();
179
197
  const title = ref('');
180
198
  //create a ref to store the opened menu items with ts type;
@@ -226,6 +244,13 @@ async function loadMenu() {
226
244
  });
227
245
  }
228
246
 
247
+ watch(route, () => {
248
+ title.value = `${coreStore.config?.title || coreStore.config?.brandName || 'Adminforth'} | ${ Object.values(route.params)[0] || route.meta.title || ' '}`;
249
+ useHead({
250
+ title: title.value,
251
+ })
252
+ });
253
+
229
254
  // initialize components based on data attribute selectors
230
255
  onMounted(async () => {
231
256
  loadMenu(); // run this in async mode
@@ -3,13 +3,6 @@ import { ref,onMounted } from 'vue'
3
3
  import { useModalStore } from '@/stores/modal';
4
4
 
5
5
  const modalStore = useModalStore();
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
6
  </script>
14
7
 
15
8
  <template>
@@ -28,10 +21,10 @@ const modalStore = useModalStore();
28
21
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
29
22
  </svg>
30
23
  <h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ modalStore?.modalContent?.content }}</h3>
31
- <button @click="()=>{modalStore.onAcceptFunction();modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
24
+ <button @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
32
25
  {{ modalStore?.modalContent?.acceptText }}
33
26
  </button>
34
- <button @click="modalStore.togleModal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 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">{{ modalStore?.modalContent?.cancelText }}</button>
27
+ <button @click="()=>{modalStore.onAcceptFunction(false);modalStore.togleModal()}" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 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">{{ modalStore?.modalContent?.cancelText }}</button>
35
28
  </div>
36
29
  </div>
37
30
  </div>
@@ -0,0 +1,65 @@
1
+ <template>
2
+
3
+
4
+ <div id="toast-default" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
5
+ <div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
6
+ <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
7
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
8
+ </svg>
9
+ <span class="sr-only">Fire icon</span>
10
+ </div>
11
+ <div v-else-if="toast.variant == 'danger'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
12
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
13
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
14
+ </svg>
15
+ <span class="sr-only">Error icon</span>
16
+ </div>
17
+ <div v-else-if="toast.variant == 'warning'"class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
18
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
19
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
20
+ </svg>
21
+ <span class="sr-only">Warning icon</span>
22
+ </div>
23
+ <div v-else class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
24
+ <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
25
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
26
+ </svg>
27
+ <span class="sr-only">Check icon</span>
28
+ </div>
29
+ <div class="ms-3 text-sm font-normal">{{toast.message}}</div>
30
+ <button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" >
31
+ <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
32
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
33
+ </svg>
34
+ </button>
35
+ </div>
36
+
37
+
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { onMounted } from 'vue';
42
+ import { useToastStore } from '@/stores/toast';
43
+ const toastStore = useToastStore();
44
+ const emit = defineEmits(['close']);
45
+ const props = defineProps<{
46
+ toast:{message: string;
47
+ variant: string;
48
+ id: string;
49
+ duration?: number;}
50
+ }>();
51
+ function closeToast() {
52
+ emit('close');
53
+ }
54
+
55
+ onMounted(() => {
56
+ setTimeout(() => {
57
+ emit('close');
58
+ }, props.toast.duration || 5000 );
59
+ });
60
+
61
+ </script>
62
+
63
+ <style lang="scss" scoped>
64
+
65
+ </style>
@@ -2,9 +2,9 @@
2
2
  <div>
3
3
 
4
4
  <span v-if="column.foreignResource">
5
- <RouterLink v-if="row[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
6
- :to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: row[column.name].pk } }">
7
- {{ row[column.name].label }}
5
+ <RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
6
+ :to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
7
+ {{ record[column.name].label }}
8
8
  </RouterLink>
9
9
  <div v-else>
10
10
  <span class="text-gray-400">-</span>
@@ -12,18 +12,18 @@
12
12
  </span>
13
13
 
14
14
  <span v-else-if="column.type === 'boolean'">
15
- <span v-if="row[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
15
+ <span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
16
16
  <span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">No</span>
17
17
  </span>
18
18
  <span v-else-if="column.enum">
19
- {{ checkEmptyValues(column.enum.find(e => e.value === row[column.name])?.label,route.meta.type) }}
19
+ {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
20
20
  </span>
21
21
  <span v-else-if="column.type === 'datetime'">
22
- {{ checkEmptyValues(formatDate(row[column.name]),route.meta.type) }}
22
+ {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
23
23
 
24
24
  </span>
25
25
  <span v-else>
26
- {{ checkEmptyValues(row[column.name],route.meta.type) }}
26
+ {{ checkEmptyValues(record[column.name],route.meta.type) }}
27
27
  </span>
28
28
  </div>
29
29
  </template>
@@ -49,7 +49,7 @@ dayjs.extend(timezone);
49
49
 
50
50
  const props = defineProps({
51
51
  column: Object,
52
- row: Object
52
+ record: Object
53
53
  });
54
54
 
55
55
 
@@ -0,0 +1,51 @@
1
+ import type { FrontendAPIInterface, ConfirmParams, AlertParams } from '../types/FrontendAPI';
2
+ import { useToastStore } from '../stores/toast';
3
+ import { useModalStore } from '../stores/modal';
4
+
5
+
6
+
7
+ declare global {
8
+ interface Window {
9
+ adminforth: {
10
+ confirm: (params: ConfirmParams) => Promise<void>;
11
+ alert: (params: AlertParams) => void;
12
+ };
13
+ }
14
+ }
15
+
16
+ export class FrontendAPI implements FrontendAPIInterface {
17
+ private toastStore:any
18
+ private modalStore:any
19
+ init() {
20
+ if (window.adminforth) {
21
+ throw new Error('adminforth already initialized');
22
+ }
23
+ this.toastStore = useToastStore();
24
+ this.modalStore = useModalStore();
25
+ console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
26
+ window.adminforth = {
27
+ confirm: this.confirm.bind(this),
28
+ alert: this.alert.bind(this)
29
+ }
30
+ }
31
+
32
+ confirm(params: ConfirmParams): Promise<void> {
33
+ return new Promise((resolve, reject) => {
34
+ this.modalStore.setModalContent({ content: params.message, acceptText: params.yes, cancelText: params.no })
35
+ this.modalStore.onAcceptFunction = resolve
36
+ this.modalStore.onCancelFunction = reject
37
+ this.modalStore.togleModal()
38
+ })
39
+ }
40
+
41
+ alert(params: AlertParams): void {
42
+ this.toastStore.addToast({
43
+ message: params.message,
44
+ variant: params.variant
45
+ })
46
+ }
47
+ }
48
+
49
+
50
+
51
+
@@ -8,6 +8,8 @@ export const useCoreStore = defineStore('core', () => {
8
8
  const config = ref({});
9
9
  const record = ref({});
10
10
  const resourceColumns = ref(null);
11
+ const resource = ref(null);
12
+
11
13
  const resourceOptions = ref(null);
12
14
  const resourceColumnsError = ref('');
13
15
  const resourceColumnsId = ref(null);
@@ -85,6 +87,7 @@ export const useCoreStore = defineStore('core', () => {
85
87
  } else {
86
88
  resourceColumns.value = res.resource.columns;
87
89
  resourceById.value[resourceId] = res.resource;
90
+ resource.value = res.resource;
88
91
  resourceOptions.value = res.resource.options;
89
92
  }
90
93
  }
@@ -129,6 +132,7 @@ export const useCoreStore = defineStore('core', () => {
129
132
  fetchResourceFull,
130
133
  resourceColumnsError,
131
134
  resourceOptions,
132
- logout
135
+ logout,
136
+ resource,
133
137
  }
134
138
  })
@@ -2,6 +2,13 @@ import { ref } from 'vue'
2
2
  import { defineStore } from 'pinia'
3
3
  import { callAdminForthApi } from '@/utils';
4
4
 
5
+ type ModalContentType = {
6
+ title?: string;
7
+ content?: string;
8
+ acceptText?: string;
9
+ cancelText?: string;
10
+ }
11
+
5
12
 
6
13
  export const useModalStore = defineStore('modal', () => {
7
14
  const modalContent = ref({
@@ -12,13 +19,17 @@ export const useModalStore = defineStore('modal', () => {
12
19
  });
13
20
  const isOpened = ref(false);
14
21
  const onAcceptFunction: any = ref(()=>{});
22
+ const onCancelFunction: any = ref(()=>{});
15
23
  function togleModal() {
16
24
  isOpened.value = !isOpened.value;
17
25
  }
18
26
  function setOnAcceptFunction(func: Function) {
19
27
  onAcceptFunction.value = func;
20
28
  }
21
- function setModalContent(content: string) {
29
+ function setOnCancelFunction(func: Function) {
30
+ onCancelFunction.value = func;
31
+ }
32
+ function setModalContent(content: ModalContentType) {
22
33
  modalContent.value = content;
23
34
  }
24
35
  function resetmodalState() {
@@ -33,6 +44,6 @@ export const useModalStore = defineStore('modal', () => {
33
44
 
34
45
  }
35
46
 
36
- return {isOpened, setModalContent, togleModal,modalContent, setOnAcceptFunction, onAcceptFunction,resetmodalState}
47
+ return {isOpened, setModalContent,onCancelFunction, togleModal,modalContent, setOnAcceptFunction, onAcceptFunction,resetmodalState,setOnCancelFunction}
37
48
 
38
49
  })
@@ -0,0 +1,15 @@
1
+ import { ref, computed } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import { callAdminForthApi } from '@/utils';
4
+ import { v1 as uuid } from 'uuid';
5
+
6
+ export const useToastStore = defineStore('toast', () => {
7
+ const toasts = ref([]);
8
+ const addToast = (toast) => {
9
+ toasts.value.push({...toast, id: uuid()});
10
+ };
11
+ const removeToast = (toast) => {
12
+ toasts.value = toasts.value.filter((t) => t.id !== toast.id);
13
+ };
14
+ return { toasts, addToast, removeToast };
15
+ });