adminforth 1.6.2-next.3 → 1.6.2-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/spa/.eslintrc.cjs +14 -0
- package/dist/spa/README.md +39 -0
- package/dist/spa/env.d.ts +1 -0
- package/dist/spa/index.html +23 -0
- package/dist/spa/package-lock.json +5062 -0
- package/dist/spa/package.json +58 -0
- package/dist/spa/postcss.config.js +6 -0
- package/dist/spa/public/assets/favicon.png +0 -0
- package/dist/spa/src/App.vue +432 -0
- package/dist/spa/src/adminforth.ts +160 -0
- package/dist/spa/src/afcl/AreaChart.vue +160 -0
- package/dist/spa/src/afcl/BarChart.vue +170 -0
- package/dist/spa/src/afcl/Button.vue +27 -0
- package/dist/spa/src/afcl/Checkbox.vue +24 -0
- package/dist/spa/src/afcl/Dropzone.vue +128 -0
- package/dist/spa/src/afcl/Input.vue +41 -0
- package/dist/spa/src/afcl/Link.vue +17 -0
- package/dist/spa/src/afcl/LinkButton.vue +25 -0
- package/dist/spa/src/afcl/PieChart.vue +175 -0
- package/dist/spa/src/afcl/ProgressBar.vue +57 -0
- package/dist/spa/src/afcl/Select.vue +246 -0
- package/dist/spa/src/afcl/Skeleton.vue +26 -0
- package/dist/spa/src/afcl/Spinner.vue +9 -0
- package/dist/spa/src/afcl/Table.vue +116 -0
- package/dist/spa/src/afcl/Tooltip.vue +43 -0
- package/dist/spa/src/afcl/VerticalTabs.vue +49 -0
- package/dist/spa/src/afcl/index.ts +20 -0
- package/dist/spa/src/assets/base.css +2 -0
- package/dist/spa/src/assets/logo.svg +19 -0
- package/dist/spa/src/components/AcceptModal.vue +44 -0
- package/dist/spa/src/components/Breadcrumbs.vue +41 -0
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +25 -0
- package/dist/spa/src/components/CustomDatePicker.vue +180 -0
- package/dist/spa/src/components/CustomDateRangePicker.vue +218 -0
- package/dist/spa/src/components/CustomRangePicker.vue +156 -0
- package/dist/spa/src/components/Filters.vue +232 -0
- package/dist/spa/src/components/GroupsTable.vue +218 -0
- package/dist/spa/src/components/HelloWorld.vue +17 -0
- package/dist/spa/src/components/MenuLink.vue +41 -0
- package/dist/spa/src/components/ResourceForm.vue +260 -0
- package/dist/spa/src/components/ResourceListTable.vue +486 -0
- package/dist/spa/src/components/ShowTable.vue +81 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +13 -0
- package/dist/spa/src/components/SkeleteLoader.vue +18 -0
- package/dist/spa/src/components/ThreeDotsMenu.vue +43 -0
- package/dist/spa/src/components/Toast.vue +78 -0
- package/dist/spa/src/components/ValueRenderer.vue +141 -0
- package/dist/spa/src/components/icons/IconCalendar.vue +5 -0
- package/dist/spa/src/components/icons/IconCommunity.vue +7 -0
- package/dist/spa/src/components/icons/IconDocumentation.vue +7 -0
- package/dist/spa/src/components/icons/IconEcosystem.vue +7 -0
- package/dist/spa/src/components/icons/IconSupport.vue +7 -0
- package/dist/spa/src/components/icons/IconTime.vue +5 -0
- package/dist/spa/src/components/icons/IconTooling.vue +19 -0
- package/dist/spa/src/composables/useFrontendApi.ts +28 -0
- package/dist/spa/src/i18n.ts +54 -0
- package/dist/spa/src/index.scss +34 -0
- package/dist/spa/src/main.ts +22 -0
- package/dist/spa/src/renderers/CompactField.vue +46 -0
- package/dist/spa/src/renderers/CompactUUID.vue +46 -0
- package/dist/spa/src/renderers/CountryFlag.vue +65 -0
- package/dist/spa/src/renderers/HumanNumber.vue +58 -0
- package/dist/spa/src/renderers/RelativeTime.vue +42 -0
- package/dist/spa/src/renderers/URL.vue +18 -0
- package/dist/spa/src/router/index.ts +70 -0
- package/dist/spa/src/spa_types/core.ts +51 -0
- package/dist/spa/src/stores/core.ts +228 -0
- package/dist/spa/src/stores/filters.ts +27 -0
- package/dist/spa/src/stores/modal.ts +48 -0
- package/dist/spa/src/stores/toast.ts +30 -0
- package/dist/spa/src/stores/user.ts +79 -0
- package/dist/spa/src/types/Adapters.ts +26 -0
- package/dist/spa/src/types/Back.ts +1344 -0
- package/dist/spa/src/types/Common.ts +940 -0
- package/dist/spa/src/types/FrontendAPI.ts +189 -0
- package/dist/spa/src/utils.ts +184 -0
- package/dist/spa/src/views/CreateView.vue +167 -0
- package/dist/spa/src/views/EditView.vue +171 -0
- package/dist/spa/src/views/ListView.vue +442 -0
- package/dist/spa/src/views/LoginView.vue +199 -0
- package/dist/spa/src/views/PageNotFound.vue +20 -0
- package/dist/spa/src/views/ResourceParent.vue +50 -0
- package/dist/spa/src/views/ShowView.vue +209 -0
- package/dist/spa/src/websocket.ts +129 -0
- package/dist/spa/tailwind.config.js +19 -0
- package/dist/spa/tsconfig.app.json +14 -0
- package/dist/spa/tsconfig.json +11 -0
- package/dist/spa/tsconfig.node.json +19 -0
- package/dist/spa/vite.config.ts +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap gap-2">
|
|
3
|
+
<input
|
|
4
|
+
:min="minFormatted"
|
|
5
|
+
:max="maxFormatted"
|
|
6
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
7
|
+
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"
|
|
8
|
+
:placeholder="$t('From')"
|
|
9
|
+
v-model="start"
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
<input
|
|
13
|
+
:min="minFormatted"
|
|
14
|
+
:max="maxFormatted"
|
|
15
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
16
|
+
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"
|
|
17
|
+
:placeholder="$t('To')"
|
|
18
|
+
v-model="end"
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
<button
|
|
22
|
+
v-if="isChanged"
|
|
23
|
+
type="button"
|
|
24
|
+
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-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
25
|
+
@click="clear">Clear
|
|
26
|
+
</button>
|
|
27
|
+
|
|
28
|
+
<div v-if="min && max" class="w-full px-2.5">
|
|
29
|
+
<vue-slider
|
|
30
|
+
class="custom-slider"
|
|
31
|
+
:dot-size="20"
|
|
32
|
+
height="7.99px"
|
|
33
|
+
:min="minFormatted"
|
|
34
|
+
:max="maxFormatted"
|
|
35
|
+
v-model="sliderValue"
|
|
36
|
+
@update:model-value="updateFromSlider($event)"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import VueSlider from 'vue-slider-component';
|
|
43
|
+
import 'vue-slider-component/theme/antd.css'
|
|
44
|
+
import {computed, onMounted, ref, watch} from "vue";
|
|
45
|
+
import debounce from 'debounce'
|
|
46
|
+
|
|
47
|
+
const props = defineProps({
|
|
48
|
+
valueStart: {
|
|
49
|
+
default: '',
|
|
50
|
+
},
|
|
51
|
+
valueEnd: {
|
|
52
|
+
default: '',
|
|
53
|
+
},
|
|
54
|
+
min: {},
|
|
55
|
+
max: {},
|
|
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
|
+
watch([minFormatted,maxFormatted], () => {
|
|
112
|
+
setSliderValues(minFormatted.value, maxFormatted.value)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const clear = () => {
|
|
116
|
+
start.value = ''
|
|
117
|
+
end.value = ''
|
|
118
|
+
setSliderValues('', '')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setSliderValues(start, end) {
|
|
122
|
+
sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<style lang="scss" scoped>
|
|
127
|
+
.custom-slider {
|
|
128
|
+
&:deep(.vue-slider-rail) {
|
|
129
|
+
background-color: rgb(229 231 235);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&:deep(.vue-slider-dot-handle) {
|
|
133
|
+
// apply bg-blue-500 to the handle when active
|
|
134
|
+
@apply bg-lightPrimary;
|
|
135
|
+
border: none;
|
|
136
|
+
box-shadow: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&:deep(.vue-slider-dot-handle:hover) {
|
|
140
|
+
@apply bg-lightPrimary;
|
|
141
|
+
filter: brightness(1.1);
|
|
142
|
+
border: none;
|
|
143
|
+
box-shadow: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
&:deep(.vue-slider-process) {
|
|
147
|
+
@apply bg-lightPrimaryOpacity;
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&:deep(.vue-slider-process:hover) {
|
|
152
|
+
filter: brightness(1.1);
|
|
153
|
+
@apply bg-lightPrimaryOpacity;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- drawer component -->
|
|
3
|
+
<div id="drawer-navigation"
|
|
4
|
+
|
|
5
|
+
class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-gray-900"
|
|
6
|
+
|
|
7
|
+
:class="show ? 'top-0 transform-none' : ''"
|
|
8
|
+
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
9
|
+
:style="{ height: `calc(100vh ` }"
|
|
10
|
+
>
|
|
11
|
+
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
|
|
12
|
+
{{ $t('Filters') }}
|
|
13
|
+
|
|
14
|
+
<button type="button" @click="$emit('hide')" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
|
15
|
+
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
|
16
|
+
<span class="sr-only">{{ $t('Close menu') }}</span>
|
|
17
|
+
</button>
|
|
18
|
+
</h5>
|
|
19
|
+
|
|
20
|
+
<div class="py-4 ">
|
|
21
|
+
<ul class="space-y-3 font-medium">
|
|
22
|
+
<li v-for="c in columnsWithFilter" :key="c">
|
|
23
|
+
<p class="dark:text-gray-400">{{ c.label }}</p>
|
|
24
|
+
|
|
25
|
+
<Select
|
|
26
|
+
v-if="c.foreignResource"
|
|
27
|
+
multiple
|
|
28
|
+
class="w-full"
|
|
29
|
+
:options="columnOptions[c.name] || []"
|
|
30
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
|
|
31
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
32
|
+
/>
|
|
33
|
+
<Select
|
|
34
|
+
multiple
|
|
35
|
+
class="w-full"
|
|
36
|
+
v-else-if="c.type === 'boolean'"
|
|
37
|
+
:options="[
|
|
38
|
+
{ label: $t('Yes'), value: true },
|
|
39
|
+
{ label: $t('No'), value: false },
|
|
40
|
+
// if field is not required, undefined might be there, and user might want to filter by it
|
|
41
|
+
...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
|
|
42
|
+
]"
|
|
43
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
|
|
44
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
<Select
|
|
48
|
+
multiple
|
|
49
|
+
class="w-full"
|
|
50
|
+
v-else-if="c.enum"
|
|
51
|
+
:options="c.enum"
|
|
52
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
|
|
53
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<input
|
|
57
|
+
v-else-if="[ 'string', 'text' ].includes(c.type)"
|
|
58
|
+
type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md 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"
|
|
59
|
+
:placeholder="$t('Search')"
|
|
60
|
+
@input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
|
|
61
|
+
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
62
|
+
>
|
|
63
|
+
|
|
64
|
+
<CustomDateRangePicker
|
|
65
|
+
v-else-if="['datetime'].includes(c.type)"
|
|
66
|
+
:column="c"
|
|
67
|
+
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
68
|
+
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
69
|
+
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
70
|
+
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<input
|
|
74
|
+
v-else-if="[ 'date', 'time' ].includes(c.type)"
|
|
75
|
+
type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md"
|
|
76
|
+
:placeholder="$t('Search datetime')"
|
|
77
|
+
@input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
|
|
78
|
+
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
79
|
+
>
|
|
80
|
+
|
|
81
|
+
<CustomRangePicker
|
|
82
|
+
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
|
|
83
|
+
:min="getFilterMinValue(c.name)"
|
|
84
|
+
:max="getFilterMaxValue(c.name)"
|
|
85
|
+
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
|
|
86
|
+
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
87
|
+
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
|
|
88
|
+
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
|
|
92
|
+
<input
|
|
93
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
94
|
+
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"
|
|
95
|
+
:placeholder="$t('From')"
|
|
96
|
+
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
97
|
+
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
98
|
+
>
|
|
99
|
+
<input
|
|
100
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
101
|
+
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"
|
|
102
|
+
:placeholder="$t('To')"
|
|
103
|
+
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
104
|
+
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
105
|
+
>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</li>
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="flex justify-end gap-2">
|
|
113
|
+
<button
|
|
114
|
+
:disabled="!filtersStore.filters.length"
|
|
115
|
+
type="button"
|
|
116
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
117
|
+
@click="clear">{{ $t('Clear all') }}</button>
|
|
118
|
+
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
123
|
+
@click="$emit('hide')">
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<script setup>
|
|
128
|
+
import { watch, computed } from 'vue'
|
|
129
|
+
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
130
|
+
import { callAdminForthApi } from '@/utils';
|
|
131
|
+
import { useRouter } from 'vue-router';
|
|
132
|
+
import { computedAsync } from '@vueuse/core'
|
|
133
|
+
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
134
|
+
import { useFiltersStore } from '@/stores/filters';
|
|
135
|
+
import Select from '@/afcl/Select.vue';
|
|
136
|
+
|
|
137
|
+
const filtersStore = useFiltersStore();
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
// props: columns
|
|
141
|
+
// add support for v-model:filers
|
|
142
|
+
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
143
|
+
const emits = defineEmits(['update:filters', 'hide']);
|
|
144
|
+
|
|
145
|
+
const router = useRouter();
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
const columnsWithFilter = computed(
|
|
149
|
+
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const columnOptions = computedAsync(async () => {
|
|
153
|
+
const ret = {};
|
|
154
|
+
if (!props.columns) {
|
|
155
|
+
return ret;
|
|
156
|
+
}
|
|
157
|
+
await Promise.all(
|
|
158
|
+
Object.values(props.columns).map(async (column) => {
|
|
159
|
+
if (column.foreignResource) {
|
|
160
|
+
const list = await callAdminForthApi({
|
|
161
|
+
method: 'POST',
|
|
162
|
+
path: `/get_resource_foreign_data`,
|
|
163
|
+
body: {
|
|
164
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
165
|
+
column: column.name,
|
|
166
|
+
limit: 1000,
|
|
167
|
+
offset: 0,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
ret[column.name] = list.items;
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return ret;
|
|
176
|
+
}, {});
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// sync 'body' class 'overflow-hidden' with show prop show
|
|
180
|
+
watch(() => props.show, (show) => {
|
|
181
|
+
if (show) {
|
|
182
|
+
document.body.classList.add('overflow-hidden');
|
|
183
|
+
} else {
|
|
184
|
+
document.body.classList.remove('overflow-hidden');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// filters is a array of objects
|
|
189
|
+
// {
|
|
190
|
+
// field: 'name',
|
|
191
|
+
// value: 'John',
|
|
192
|
+
// operator: 'like'
|
|
193
|
+
// }
|
|
194
|
+
|
|
195
|
+
function setFilterItem({ column, operator, value }) {
|
|
196
|
+
|
|
197
|
+
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
198
|
+
if (value === undefined) {
|
|
199
|
+
if (index !== -1) {
|
|
200
|
+
filtersStore.filters.splice(index, 1);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
if (index === -1) {
|
|
204
|
+
filtersStore.setFilter({ field: column.name, value, operator });
|
|
205
|
+
} else {
|
|
206
|
+
filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getFilterItem({ column, operator }) {
|
|
213
|
+
return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function clear() {
|
|
217
|
+
filtersStore.clearFilters();
|
|
218
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getFilterMinValue(columnName) {
|
|
222
|
+
if(props.columnsMinMax && props.columnsMinMax[columnName]) {
|
|
223
|
+
return props.columnsMinMax[columnName]?.min
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getFilterMaxValue(columnName) {
|
|
228
|
+
if(props.columnsMinMax && props.columnsMinMax[columnName]) {
|
|
229
|
+
return props.columnsMinMax[columnName]?.max
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
</script>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
|
|
3
|
+
<div v-if="group.groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
|
|
4
|
+
{{ group.groupName }}
|
|
5
|
+
</div>
|
|
6
|
+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
7
|
+
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
|
|
8
|
+
<tr>
|
|
9
|
+
<th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
|
|
10
|
+
{{ $t('Field') }}
|
|
11
|
+
</th>
|
|
12
|
+
<th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-6 py-3 hidden md:table-cell">
|
|
13
|
+
{{ $t('Value') }}
|
|
14
|
+
</th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
<tbody>
|
|
18
|
+
<tr
|
|
19
|
+
v-for="(column, i) in group.columns"
|
|
20
|
+
:key="column.name"
|
|
21
|
+
v-if="currentValues !== null"
|
|
22
|
+
class="bg-ligftForm dark:bg-gray-800 dark:border-gray-700 block md:table-row"
|
|
23
|
+
:class="{ 'border-b': i !== group.columns.length - 1}"
|
|
24
|
+
>
|
|
25
|
+
<td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
|
|
26
|
+
:class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
|
|
27
|
+
<span class="flex items-center gap-1">
|
|
28
|
+
{{ column.label }}
|
|
29
|
+
<Tooltip v-if="column.required[mode]">
|
|
30
|
+
|
|
31
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
32
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<template #tooltip>
|
|
36
|
+
{{ $t('Required field') }}
|
|
37
|
+
</template>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
</span>
|
|
40
|
+
</td>
|
|
41
|
+
<td class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell rounded-br-lg "
|
|
42
|
+
:class="{'rounded-br-lg': i === group.columns.length - 1}">
|
|
43
|
+
<template v-if="column?.components?.[props.source]?.file">
|
|
44
|
+
<component
|
|
45
|
+
:is="getCustomComponent(column.components[props.source])"
|
|
46
|
+
:column="column"
|
|
47
|
+
:value="currentValues[column.name]"
|
|
48
|
+
@update:value="setCurrentValue(column.name, $event)"
|
|
49
|
+
:meta="column.components[props.source].meta"
|
|
50
|
+
:record="currentValues"
|
|
51
|
+
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
52
|
+
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
<template v-else>
|
|
56
|
+
<Select
|
|
57
|
+
class="w-full"
|
|
58
|
+
v-if="column.foreignResource"
|
|
59
|
+
:options="columnOptions[column.name] || []"
|
|
60
|
+
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
61
|
+
:modelValue="currentValues[column.name]"
|
|
62
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
63
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
64
|
+
></Select>
|
|
65
|
+
<Select
|
|
66
|
+
class="w-full"
|
|
67
|
+
v-else-if="column.enum"
|
|
68
|
+
:options="column.enum"
|
|
69
|
+
:modelValue="currentValues[column.name]"
|
|
70
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
71
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
72
|
+
></Select>
|
|
73
|
+
<Select
|
|
74
|
+
class="w-full"
|
|
75
|
+
v-else-if="column.type === 'boolean'"
|
|
76
|
+
:options="getBooleanOptions(column)"
|
|
77
|
+
:modelValue="currentValues[column.name]"
|
|
78
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
79
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
80
|
+
></Select>
|
|
81
|
+
<input
|
|
82
|
+
v-else-if="['integer'].includes(column.type)"
|
|
83
|
+
type="number"
|
|
84
|
+
step="1"
|
|
85
|
+
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-40 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"
|
|
86
|
+
placeholder="0"
|
|
87
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
88
|
+
:value="currentValues[column.name]"
|
|
89
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
90
|
+
>
|
|
91
|
+
<CustomDatePicker
|
|
92
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
93
|
+
:column="column"
|
|
94
|
+
:valueStart="currentValues[column.name]"
|
|
95
|
+
auto-hide
|
|
96
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
97
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
98
|
+
/>
|
|
99
|
+
<input
|
|
100
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
101
|
+
type="number"
|
|
102
|
+
step="0.1"
|
|
103
|
+
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-40 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"
|
|
104
|
+
placeholder="0.0"
|
|
105
|
+
:value="currentValues[column.name]"
|
|
106
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
107
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
108
|
+
/>
|
|
109
|
+
<textarea
|
|
110
|
+
v-else-if="['text', 'richtext'].includes(column.type)"
|
|
111
|
+
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"
|
|
112
|
+
:placeholder="$t('Text')"
|
|
113
|
+
:value="currentValues[column.name]"
|
|
114
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
115
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
116
|
+
>
|
|
117
|
+
</textarea>
|
|
118
|
+
<textarea
|
|
119
|
+
v-else-if="['json'].includes(column.type)"
|
|
120
|
+
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"
|
|
121
|
+
:placeholder="$t('Text')"
|
|
122
|
+
:value="currentValues[column.name]"
|
|
123
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
124
|
+
>
|
|
125
|
+
</textarea>
|
|
126
|
+
<input
|
|
127
|
+
v-else
|
|
128
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
129
|
+
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"
|
|
130
|
+
:placeholder="$t('Text')"
|
|
131
|
+
:value="currentValues[column.name]"
|
|
132
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
133
|
+
autocomplete="false"
|
|
134
|
+
data-lpignore="true"
|
|
135
|
+
readonly
|
|
136
|
+
ref="readonlyInputs"
|
|
137
|
+
@focus="onFocusHandler($event, column, source)"
|
|
138
|
+
>
|
|
139
|
+
|
|
140
|
+
<button
|
|
141
|
+
v-if="column.masked"
|
|
142
|
+
type="button"
|
|
143
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
144
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
145
|
+
>
|
|
146
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
147
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
148
|
+
</button>
|
|
149
|
+
</template>
|
|
150
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
151
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<script setup lang="ts">
|
|
160
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
161
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
162
|
+
import Select from '@/afcl/Select.vue';
|
|
163
|
+
import { getCustomComponent } from '@/utils';
|
|
164
|
+
import { Tooltip } from '@/afcl';
|
|
165
|
+
import { ref, computed, watch, type Ref } from 'vue';
|
|
166
|
+
import { useI18n } from 'vue-i18n';
|
|
167
|
+
|
|
168
|
+
const { t } = useI18n();
|
|
169
|
+
|
|
170
|
+
const props = defineProps<{
|
|
171
|
+
source: 'create' | 'edit',
|
|
172
|
+
group: any,
|
|
173
|
+
mode: string,
|
|
174
|
+
validating: boolean,
|
|
175
|
+
currentValues: any,
|
|
176
|
+
unmasked: any,
|
|
177
|
+
columnError: (column: any) => string,
|
|
178
|
+
setCurrentValue: (columnName: string, value: any) => void,
|
|
179
|
+
columnOptions: any,
|
|
180
|
+
}>();
|
|
181
|
+
|
|
182
|
+
const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
|
|
183
|
+
const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
|
|
184
|
+
|
|
185
|
+
const getBooleanOptions = (column: any) => {
|
|
186
|
+
const options: Array<{ label: string; value: boolean | null }> = [
|
|
187
|
+
{ label: t('Yes'), value: true },
|
|
188
|
+
{ label: t('No'), value: false },
|
|
189
|
+
];
|
|
190
|
+
if (!column.required[props.mode]) {
|
|
191
|
+
options.push({ label: t('Unset'), value: null });
|
|
192
|
+
}
|
|
193
|
+
return options;
|
|
194
|
+
};
|
|
195
|
+
function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
|
|
196
|
+
const focusedInput = event.target as HTMLInputElement;
|
|
197
|
+
if(!focusedInput) return;
|
|
198
|
+
if (column.editReadonly && source === 'edit') return;
|
|
199
|
+
else {
|
|
200
|
+
focusedInput.removeAttribute('readonly');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
watch(customComponentsInValidity, (newVal) => {
|
|
211
|
+
emit('update:customComponentsInValidity', newVal);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
watch(customComponentsEmptiness, (newVal) => {
|
|
215
|
+
emit('update:customComponentsEmptiness', newVal);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
msg: string
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="greetings">
|
|
9
|
+
<h1 class="green">{{ msg }}</h1>
|
|
10
|
+
<h3>
|
|
11
|
+
{{ $t('You’ve successfully created a project with') }}
|
|
12
|
+
</h3>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<RouterLink
|
|
3
|
+
:to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
|
|
4
|
+
class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
|
|
5
|
+
:class="{
|
|
6
|
+
'px-4': isChild,
|
|
7
|
+
'px-2': !isChild,
|
|
8
|
+
'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
|
|
9
|
+
($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
|
|
10
|
+
($route.name === item.path)
|
|
11
|
+
}"
|
|
12
|
+
>
|
|
13
|
+
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
|
|
14
|
+
<span class="ms-3">{{ item.label }}</span>
|
|
15
|
+
<span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
16
|
+
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent"
|
|
17
|
+
>
|
|
18
|
+
|
|
19
|
+
<Tooltip v-if="item.badgeTooltip">
|
|
20
|
+
{{ item.badge }}
|
|
21
|
+
|
|
22
|
+
<template #tooltip>
|
|
23
|
+
{{ item.badgeTooltip }}
|
|
24
|
+
</template>
|
|
25
|
+
</Tooltip>
|
|
26
|
+
<template v-else>
|
|
27
|
+
{{ item.badge }}
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
</span>
|
|
31
|
+
|
|
32
|
+
</RouterLink>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { getIcon } from '@/utils';
|
|
37
|
+
import { Tooltip } from '@/afcl';
|
|
38
|
+
const props = defineProps(['item', 'isChild']);
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
</script>
|