adminforth 1.1.21 → 1.1.23

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/plugins/AccessControl/index.js +56 -3
  2. package/dist/spa/index.html +2 -2
  3. package/dist/spa/package-lock.json +189 -1
  4. package/dist/spa/package.json +6 -1
  5. package/dist/spa/spa/src/composables/useFrontendApi.ts +12 -0
  6. package/dist/spa/spa/src/views/ShowView.vue +22 -27
  7. package/dist/spa/src/App.vue +76 -57
  8. package/dist/spa/src/components/AcceptModal.vue +1 -1
  9. package/dist/spa/src/components/CustomDatePicker.vue +3 -3
  10. package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
  11. package/dist/spa/src/components/CustomRangePicker.vue +148 -0
  12. package/dist/spa/src/components/Dropdown.vue +13 -4
  13. package/dist/spa/src/components/Filters.vue +55 -22
  14. package/dist/spa/src/components/MenuLink.vue +2 -2
  15. package/dist/spa/src/components/ResourceForm.vue +157 -99
  16. package/dist/spa/src/components/ValueRenderer.vue +11 -1
  17. package/dist/spa/src/router/index.ts +3 -2
  18. package/dist/spa/src/stores/core.ts +22 -33
  19. package/dist/spa/src/utils.ts +5 -3
  20. package/dist/spa/src/views/CreateView.vue +15 -7
  21. package/dist/spa/src/views/EditView.vue +36 -8
  22. package/dist/spa/src/views/ListView.vue +35 -23
  23. package/dist/spa/src/views/LoginView.vue +1 -1
  24. package/dist/spa/src/views/ResourceParent.vue +1 -1
  25. package/dist/spa/src/views/ShowView.vue +20 -9
  26. package/dist/spa/tailwind.config.js +5 -2
  27. package/index.ts +0 -1
  28. package/package.json +1 -1
  29. package/spa/src/composables/useFrontendApi.ts +12 -0
  30. package/spa/src/views/ShowView.vue +22 -27
  31. package/dist/plugins/AuditLog/index.js +0 -66
  32. package/dist/plugins/AuditLog/types.js +0 -1
  33. package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
  34. package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
  35. package/dist/spa/spa/src/views/HomeView.vue +0 -8
  36. package/dist/spa/src/views/HomeView.vue +0 -8
  37. package/plugins/AccessControl/index.ts +0 -29
  38. package/plugins/AccessControl/types.ts +0 -7
@@ -16,7 +16,7 @@ const modalStore = useModalStore();
16
16
  <Teleport to="body">
17
17
  <div v-if="modalStore.isOpened" class="bg-gray-900/50 dark:bg-gray-900/80 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
18
18
  <div class="relative p-4 w-full max-w-md max-h-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 " >
19
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
19
+ <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black">
20
20
  <button type="button"@click="modalStore.togleModal" class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
21
21
  <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
22
22
  <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"/>
@@ -5,7 +5,7 @@
5
5
  <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
6
6
 
7
7
  <div class="relative">
8
- <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
8
+ <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
9
9
  <IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
10
10
  </div>
11
11
 
@@ -97,7 +97,7 @@ const start = computed(() => {
97
97
  })
98
98
 
99
99
  function updateFromProps() {
100
- if (props.valueStart === undefined) {
100
+ if (!props.valueStart) {
101
101
  datepickerStartEl.value.value = '';
102
102
  startTime.value = '';
103
103
  }
@@ -157,5 +157,5 @@ onMounted(() => {
157
157
 
158
158
  onBeforeUnmount(() => {
159
159
  removeChangeDateListener();
160
- })
160
+ });
161
161
  </script>
@@ -26,7 +26,7 @@
26
26
  <div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: !showTimeInputs}">
27
27
  <div>
28
28
  <div class="relative">
29
- <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
29
+ <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
30
30
  <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
31
31
  </div>
32
32
 
@@ -132,11 +132,11 @@ const end = computed(() => {
132
132
  })
133
133
 
134
134
  function updateFromProps() {
135
- if (props.valueStart === undefined) {
135
+ if (!props.valueStart) {
136
136
  datepickerStartEl.value.value = '';
137
137
  startTime.value = '';
138
138
  }
139
- if (props.valueEnd === undefined) {
139
+ if (!props.valueEnd) {
140
140
  datepickerEndEl.value.value = '';
141
141
  endTime.value = '';
142
142
  }
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div class="flex flex-wrap gap-2">
3
+ <input
4
+ type="number" aria-describedby="helper-text-explanation"
5
+ 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-20 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"
6
+ placeholder="From"
7
+ v-model="start"
8
+ >
9
+
10
+ <input
11
+ type="number" aria-describedby="helper-text-explanation"
12
+ 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-20 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"
13
+ placeholder="To"
14
+ v-model="end"
15
+ >
16
+
17
+ <button
18
+ v-if="isChanged"
19
+ type="button"
20
+ class="flex items-center p-0.5 ml-auto 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-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 disabled:opacity-50 disabled:cursor-not-allowed"
21
+ @click="clear">Clear
22
+ </button>
23
+
24
+ <div class="w-full">
25
+ <vue-slider
26
+ class="custom-slider"
27
+ :dot-size="20"
28
+ height="7.99px"
29
+ :min="minFormatted"
30
+ :max="maxFormatted"
31
+ v-model="sliderValue"
32
+ @update:model-value="updateFromSlider($event)"
33
+ />
34
+ </div>
35
+ </div>
36
+ </template>
37
+ <script setup lang="ts">
38
+ import VueSlider from 'vue-slider-component';
39
+ import 'vue-slider-component/theme/antd.css'
40
+ import {computed, onMounted, ref, watch} from "vue";
41
+ import debounce from 'debounce'
42
+
43
+ const props = defineProps({
44
+ valueStart: {
45
+ default: '',
46
+ },
47
+ valueEnd: {
48
+ default: '',
49
+ },
50
+ min: {
51
+ default: 0,
52
+ },
53
+ max: {
54
+ default: 10,
55
+ },
56
+ });
57
+
58
+ const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
59
+
60
+ const minFormatted = computed(() => Math.floor(props.min));
61
+ const maxFormatted = computed(() => Math.ceil(props.max));
62
+
63
+ const isChanged = computed(() => {
64
+ return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
65
+ });
66
+
67
+ const start = ref(props.valueStart);
68
+ const end = ref(props.valueEnd);
69
+
70
+ const sliderValue = ref([minFormatted.value, maxFormatted.value]);
71
+
72
+ const updateFromSlider =
73
+ debounce((value: [number, number]) => {
74
+ start.value = value[0] === minFormatted.value ? '': value[0];
75
+ end.value = value[1] === maxFormatted.value ? '': value[1];
76
+ }, 500);
77
+
78
+ onMounted(() => {
79
+ updateStartFromProps();
80
+ updateEndFromProps();
81
+
82
+ watch(() => props.valueStart, (value) => {
83
+ updateStartFromProps();
84
+ });
85
+
86
+ watch(() => props.valueEnd, (value) => {
87
+ updateEndFromProps();
88
+ });
89
+ })
90
+
91
+ function updateStartFromProps() {
92
+ start.value = props.valueStart;
93
+ setSliderValues(start.value, end.value)
94
+ }
95
+
96
+ function updateEndFromProps() {
97
+ end.value = props.valueEnd;
98
+ setSliderValues(start.value, end.value)
99
+ }
100
+
101
+ watch(start, () => {
102
+ console.log('⚡ emit', start.value)
103
+ emit('update:valueStart', start.value)
104
+ })
105
+
106
+ watch(end, () => {
107
+ console.log('⚡ emit', end.value)
108
+ emit('update:valueEnd', end.value);
109
+ })
110
+
111
+ const clear = () => {
112
+ start.value = ''
113
+ end.value = ''
114
+ setSliderValues('', '')
115
+ }
116
+
117
+ function setSliderValues(start, end) {
118
+ sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
119
+ }
120
+ </script>
121
+
122
+ <style lang="scss" scoped>
123
+ .custom-slider {
124
+ &:deep(.vue-slider-rail) {
125
+ background-color: rgb(229 231 235);
126
+ }
127
+
128
+ &:deep(.vue-slider-dot-handle) {
129
+ background-color: #1c64f2;
130
+ border: none;
131
+ box-shadow: none;
132
+ }
133
+
134
+ &:deep(.vue-slider-dot-handle:hover) {
135
+ background-color: #1c64f2;
136
+ border: none;
137
+ box-shadow: none;
138
+ }
139
+
140
+ &:deep(.vue-slider-process) {
141
+ background-color: rgb(225 239 254);
142
+ }
143
+
144
+ &:deep(.vue-slider-process:hover) {
145
+ background-color: rgb(225 239 254);
146
+ }
147
+ }
148
+ </style>
@@ -9,7 +9,8 @@
9
9
  :placeholder="selectedItems.length ? '' : 'Select...'"
10
10
  />
11
11
  <div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
12
- <div v-for="item in selectedItems" :key="item.name" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
12
+ {{ }}
13
+ <div v-for="item in selectedItems" :key="item?.name" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
13
14
  <span>{{ item.label }}</span>
14
15
  <button
15
16
  type="button"
@@ -34,7 +35,7 @@
34
35
  <IconCaretUpSolid v-else class="h-5 w-5 text-gray-400" />
35
36
  </div>
36
37
  </div>
37
- <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
38
+ <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
38
39
  <div
39
40
  v-for="item in filteredItems"
40
41
  :key="item.value"
@@ -76,10 +77,14 @@ const showDropdown = ref(false);
76
77
  const selectedItems = ref([]);
77
78
 
78
79
  function updateFromProps() {
79
- console.log('⚡ updateFromProps', props.modelValue)
80
80
  if (props.modelValue !== undefined) {
81
81
  if (props.single) {
82
- selectedItems.value = [props.options.find(item => item.value === props.modelValue)];
82
+ const el = props.options.find(item => item.value === props.modelValue);
83
+ if (el) {
84
+ selectedItems.value = [el];
85
+ } else {
86
+ selectedItems.value = [];
87
+ }
83
88
  } else {
84
89
  selectedItems.value = props.options.filter(item => props.modelValue.includes(item.value));
85
90
  }
@@ -93,6 +98,10 @@ onMounted(() => {
93
98
  updateFromProps();
94
99
  });
95
100
 
101
+ watch(() => props.options, () => {
102
+ updateFromProps();
103
+ });
104
+
96
105
  addClickListener();
97
106
 
98
107
  });
@@ -2,7 +2,7 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
5
+ class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
@@ -22,8 +22,14 @@
22
22
  <li v-for="c in columnsWithFilter" :key="c">
23
23
  <p class="dark:text-gray-400">{{ c.label }}</p>
24
24
 
25
- <Dropdown
26
- v-if="c.type === 'boolean'"
25
+ <Dropdown
26
+ v-if="c.foreignResource"
27
+ :options="columnOptions[c.name] || []"
28
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
+ :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
+ />
31
+ <Dropdown
32
+ v-else-if="c.type === 'boolean'"
27
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
28
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
35
  :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
@@ -62,22 +68,15 @@
62
68
  :value="getFilterItem({ column: c, operator: 'ilike' })"
63
69
  >
64
70
 
65
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
66
- <input
67
- type="number" aria-describedby="helper-text-explanation"
68
- 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-20 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"
69
- placeholder="From"
70
- @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
71
- :value="getFilterItem({ column: c, operator: 'gte' })"
72
- >
73
- <input
74
- type="number" aria-describedby="helper-text-explanation"
75
- 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-20 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"
76
- placeholder="To"
77
- @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
78
- :value="getFilterItem({ column: c, operator: 'lte' })"
79
- >
80
- </div>
71
+ <CustomRangePicker
72
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
73
+ :min="c.min"
74
+ :max="c.max"
75
+ :valueStart="getFilterItem({ column: c, operator: 'gte' })"
76
+ @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
77
+ :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
78
+ @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
79
+ />
81
80
 
82
81
  </li>
83
82
  </ul>
@@ -93,25 +92,59 @@
93
92
  </div>
94
93
  </div>
95
94
 
96
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
95
+ <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-50"
97
96
  @click="$emit('hide')">
98
97
  </div>
99
98
  </template>
100
99
 
101
100
  <script setup>
102
- import { watch, computed } from 'vue'
101
+ import { watch, computed, ref, onMounted } from 'vue'
103
102
  import Dropdown from '@/components/Dropdown.vue';
104
103
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
104
+ import { callAdminForthApi } from '@/utils';
105
+ import { useRouter } from 'vue-router';
106
+ import { computedAsync } from '@vueuse/core'
107
+ import CustomRangePicker from "@/components/CustomRangePicker.vue";
105
108
 
106
109
  // props: columns
107
110
  // add support for v-model:filers
108
111
  const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
109
112
  const emits = defineEmits(['update:filters', 'hide']);
110
113
 
111
- const columnsWithFilter = computed(
114
+ const router = useRouter();
115
+
116
+
117
+ const columnsWithFilter = computed(
112
118
  () => props.columns?.filter(column => column.showIn.includes('filter')) || []
113
119
  );
114
120
 
121
+ const columnOptions = computedAsync(async () => {
122
+ const ret = {};
123
+ if (!props.columns) {
124
+ return ret;
125
+ }
126
+ await Promise.all(
127
+ Object.values(props.columns).map(async (column) => {
128
+ if (column.foreignResource) {
129
+ const list = await callAdminForthApi({
130
+ method: 'POST',
131
+ path: `/get_resource_foreign_data`,
132
+ body: {
133
+ resourceId: router.currentRoute.value.params.resourceId,
134
+ column: column.name,
135
+ limit: 1000,
136
+ offset: 0,
137
+ },
138
+ });
139
+ ret[column.name] = list.items;
140
+ }
141
+ })
142
+ );
143
+
144
+ return ret;
145
+ }, {});
146
+
147
+
115
148
  // sync 'body' class 'overflow-hidden' with show prop show
116
149
  watch(() => props.show, (show) => {
117
150
  if (show) {
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex items-center py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"
4
+ class="flex items-center py-2 text-nav-menu-text rounded-default hover:bg-nav-menu-bg-hover active:bg-nav-menu-bg-active" role="menuitem"
5
5
  :class="{
6
6
  'px-4': isChild,
7
7
  'px-2': !isChild,
8
- 'bg-blue-100 dark:bg-gray-700': item.resourceId ?
8
+ 'bg-nav-menu-bg-active dark:bg-gray-700': item.resourceId ?
9
9
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
10
  ($route.name === item.path)
11
11
  }"