@citruslime/ui 1.2.1-beta.0 → 2.0.0-beta.3
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/.eslintrc.js +8 -2
- package/dist/@types/appUser.d.ts +1 -0
- package/dist/@types/components/grid/column.d.ts +2 -1
- package/dist/@types/components/header/index.d.ts +0 -1
- package/dist/@types/components/{header/navigation.d.ts → navigation/index.d.ts} +7 -4
- package/dist/@types/index.d.ts +1 -0
- package/dist/components/index.d.ts +17 -14
- package/dist/main.d.ts +1 -1
- package/dist/style.css +1 -1
- package/dist/theme.js +2 -4
- package/dist/ui.es.js +1 -1
- package/dist/ui.umd.js +1 -1
- package/package.json +7 -4
- package/src/components/accordion/cl-ui-accordion.vue +89 -0
- package/src/components/app/cl-ui-app.vue +35 -0
- package/src/components/button/{button.vue → cl-ui-button.vue} +26 -6
- package/src/components/calendar/cl-ui-calendar.vue +277 -0
- package/src/components/card/{card.vue → cl-ui-card.vue} +17 -1
- package/src/components/combo-box/cl-ui-combo-box.vue +357 -0
- package/src/components/combo-box/search-container/cl-ui-combo-box-search.vue +279 -0
- package/src/components/combo-box/search-container/{header-option/header-option.vue → header/cl-ui-combo-box-header.vue} +17 -2
- package/src/components/combo-box/search-container/selectable/cl-ui-combo-box-selectable.vue +99 -0
- package/src/components/footer/{footer.vue → cl-ui-footer.vue} +10 -2
- package/src/components/grid/cell/{cell.vue → cl-ui-grid-cell.vue} +90 -1
- package/src/components/grid/cl-ui-grid.vue +477 -0
- package/src/components/grid/filter/cl-ui-grid-filter.vue +270 -0
- package/src/components/grid/footer/{footer.vue → cl-ui-grid-footer.vue} +100 -5
- package/src/components/grid/header/cl-ui-grid-header.vue +76 -0
- package/src/components/grid/view-manager/cl-ui-grid-view-manager.vue +145 -0
- package/src/components/header/cl-ui-header.vue +11 -0
- package/src/components/header-helper/cl-ui-header-helper.vue +50 -0
- package/src/components/language-switcher/{language-switcher.vue → cl-ui-language-switcher.vue} +49 -3
- package/src/components/loading-spinner/cl-ui-loading-spinner.vue +16 -0
- package/src/components/login/{login.vue → cl-ui-login.vue} +101 -19
- package/src/components/modal/{modal.vue → cl-ui-modal.vue} +74 -2
- package/src/components/navigation/cl-ui-navigation.vue +124 -0
- package/src/components/notification/{notification.vue → cl-ui-notification.vue} +21 -2
- package/src/components/slider/cl-ui-slider.vue +145 -0
- package/src/components/accordion/accordion.vue +0 -30
- package/src/components/calendar/calendar.vue +0 -35
- package/src/components/combo-box/combo-box.vue +0 -79
- package/src/components/combo-box/search-container/search-container.vue +0 -57
- package/src/components/combo-box/search-container/selectable-option/selectable-option.vue +0 -27
- package/src/components/grid/filter/filter.vue +0 -93
- package/src/components/grid/grid.vue +0 -194
- package/src/components/grid/header/header.vue +0 -39
- package/src/components/grid/view-manager/view-manager.vue +0 -73
- package/src/components/header/header-helper/header-helper.vue +0 -95
- package/src/components/header/header.vue +0 -33
- package/src/components/header/navigation/navigation.vue +0 -84
- package/src/components/loading-spinner/loading-spinner.vue +0 -8
- package/src/components/slider/slider.vue +0 -41
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { FilterMethod, FilterMethodType, FilterOperation, FilterRequest, GridColumn, GridColumnType, GridFilterLocalisations, NumberFormat } from '../../../@types';
|
|
5
|
+
import { copy } from '../../../utils/copy';
|
|
6
|
+
import { useDebouncer } from '../../../utils/debouncer';
|
|
7
|
+
import { filterMethods } from '../../../utils/filter-methods';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
request: FilterRequest;
|
|
11
|
+
currentLocale?: string;
|
|
12
|
+
localisations: GridFilterLocalisations;
|
|
13
|
+
column: GridColumn;
|
|
14
|
+
firstHalf?: boolean;
|
|
15
|
+
}>(), {
|
|
16
|
+
currentLocale: 'en-GB',
|
|
17
|
+
firstHalf: true
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits({
|
|
21
|
+
'update:request': null
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { debounce } = useDebouncer(750);
|
|
25
|
+
|
|
26
|
+
const allowedFilterMethods = computed<FilterMethod[]>(() => {
|
|
27
|
+
const methods = getAllowedMethods();
|
|
28
|
+
|
|
29
|
+
methods.forEach(m => {
|
|
30
|
+
m.description = props.localisations[m.method as keyof GridFilterLocalisations];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return methods;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const currentFilterMethod = computed<FilterMethod | undefined>(() => {
|
|
37
|
+
let filterMethod: string | undefined = props.request.filters.find(f => f.filterOnColumn === props.column.field)?.filterMethod;
|
|
38
|
+
|
|
39
|
+
if (typeof filterMethod === 'undefined') {
|
|
40
|
+
filterMethod = FilterOperation[getDefaultFilterOperation()];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return allowedFilterMethods.value.find(m => m.method.toLowerCase() === filterMethod?.removeNonAlphanumeric()?.toLowerCase());
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const currentFilter = computed<string>(() => props.request.filters.find(f => f.filterOnColumn === props.column.field)?.filterValue ?? '');
|
|
47
|
+
const currentNumberFilter = computed<number>(() => Number(currentFilter.value));
|
|
48
|
+
const currentDateFilter = computed<Date | null>(() => currentFilter.value !== '' ? new Date(currentFilter.value) : null);
|
|
49
|
+
|
|
50
|
+
const decimalSeparator = computed<string>(() =>
|
|
51
|
+
Intl.NumberFormat(props.currentLocale)
|
|
52
|
+
.format(1.1)
|
|
53
|
+
.replace(/[0-9]+/g, ''));
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets the default operation for the type of the current column.
|
|
57
|
+
*
|
|
58
|
+
* @returns The default filter operation.
|
|
59
|
+
*/
|
|
60
|
+
function getDefaultFilterOperation (): FilterOperation {
|
|
61
|
+
return props.column.type === GridColumnType.STRING || props.column.slotType === GridColumnType.STRING ?
|
|
62
|
+
FilterOperation.CONTAINS :
|
|
63
|
+
props.column.type === GridColumnType.DATETIME || props.column.slotType === GridColumnType.DATETIME ? FilterOperation.LESS_THAN : FilterOperation.EQUAL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets a list of allowed filter methods, for the current column, based on the column type.
|
|
68
|
+
*
|
|
69
|
+
* @returns The available filter methods.
|
|
70
|
+
*/
|
|
71
|
+
function getAllowedMethods (): FilterMethod[] {
|
|
72
|
+
let result: FilterMethod[] = [];
|
|
73
|
+
|
|
74
|
+
if (props.column.type !== GridColumnType.SLOT) {
|
|
75
|
+
result = filterMethods.filter(f => f.allowedTypes.includes(props.column.type));
|
|
76
|
+
}
|
|
77
|
+
else if (typeof props.column.slotType !== 'undefined' && typeof props.column.field !== 'undefined') {
|
|
78
|
+
result = filterMethods.filter(f => f.allowedTypes.includes(props.column.slotType as Exclude<GridColumnType, GridColumnType.SLOT>));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Emits an event to update the current filter method, for the current column.
|
|
86
|
+
*
|
|
87
|
+
* @param filterMethod The new filter method, for the column.
|
|
88
|
+
*/
|
|
89
|
+
function setFilterMethod (filterMethod: FilterMethodType): void {
|
|
90
|
+
const filterRequest = copy(props.request);
|
|
91
|
+
const index: number = filterRequest.filters.findIndex(f => f.filterOnColumn === props.column.field);
|
|
92
|
+
|
|
93
|
+
if (index >= 0) {
|
|
94
|
+
filterRequest.filters[index].filterOperation = FilterOperation[filterMethod.toUpperCase() as keyof typeof FilterOperation];
|
|
95
|
+
filterRequest.filters[index].filterMethod = filterMethod.toUpperCase() as FilterMethodType;
|
|
96
|
+
}
|
|
97
|
+
else if (typeof props.column.field !== 'undefined') {
|
|
98
|
+
filterRequest.filters.push({
|
|
99
|
+
filterOnColumn: props.column.field,
|
|
100
|
+
filterMethod: filterMethod.toUpperCase() as FilterMethodType,
|
|
101
|
+
filterOperation: FilterOperation[filterMethod.toUpperCase() as keyof typeof FilterOperation],
|
|
102
|
+
filterValue: ''
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
emit('update:request', filterRequest);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Emits an event to update the current filter value, for the current column.
|
|
111
|
+
*
|
|
112
|
+
* @param target The filter element.
|
|
113
|
+
* @param defaultValue The value to default to, if the filter element is null.
|
|
114
|
+
*/
|
|
115
|
+
function setFilter (target: EventTarget | null, defaultValue?: string): void {
|
|
116
|
+
const value = ((target as HTMLInputElement)?.value ?? defaultValue).trim();
|
|
117
|
+
const filterRequest = copy(props.request);
|
|
118
|
+
const index: number = filterRequest.filters.findIndex(f => f.filterOnColumn === props.column.field);
|
|
119
|
+
|
|
120
|
+
let preventEmit = false;
|
|
121
|
+
|
|
122
|
+
filterRequest.pageNumber = 1;
|
|
123
|
+
|
|
124
|
+
if (index >= 0) {
|
|
125
|
+
preventEmit = filterRequest.filters[index].filterValue === value;
|
|
126
|
+
|
|
127
|
+
filterRequest.filters[index].filterValue = value;
|
|
128
|
+
}
|
|
129
|
+
else if (value !== '' && typeof props.column.field !== 'undefined') {
|
|
130
|
+
const defaultFilterOperation: FilterOperation = getDefaultFilterOperation();
|
|
131
|
+
|
|
132
|
+
filterRequest.filters.push({
|
|
133
|
+
filterOnColumn: props.column.field,
|
|
134
|
+
filterMethod: FilterOperation[defaultFilterOperation] as FilterMethodType,
|
|
135
|
+
filterOperation: defaultFilterOperation,
|
|
136
|
+
filterValue: value
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (preventEmit === false) {
|
|
141
|
+
emit('update:request', filterRequest);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Updates the current filter value, for the current number column.
|
|
147
|
+
*
|
|
148
|
+
* @param target The filter element.
|
|
149
|
+
*/
|
|
150
|
+
function setNumberFilter (target: EventTarget | null): void {
|
|
151
|
+
let inputValue: string = (target as HTMLInputElement | null)?.value?.replace(new RegExp(`[^0-9\\${decimalSeparator.value}]`, 'g'), '') ?? '';
|
|
152
|
+
|
|
153
|
+
let value: string | number = '';
|
|
154
|
+
if (props.column.format === NumberFormat.INTEGER) {
|
|
155
|
+
value = parseInt(inputValue);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
inputValue = inputValue.replace(decimalSeparator.value, '.');
|
|
159
|
+
value = parseFloat(inputValue);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof value === 'number' && isNaN(value)) {
|
|
163
|
+
value = '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setFilter(null, value.toString());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Updates the current filter value, for the current Date/DateTime column.
|
|
171
|
+
*
|
|
172
|
+
* @param value The new filter value, for the column.
|
|
173
|
+
*/
|
|
174
|
+
function setDateFilter (value: Date | null): void {
|
|
175
|
+
setFilter(null, value?.toISOString() ?? '');
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<div v-if="column.filterable === undefined || column.filterable === true"
|
|
181
|
+
class="border-grey-2 flex rounded w-full"
|
|
182
|
+
:class="{
|
|
183
|
+
'border-b p-5': column.type === GridColumnType.SLOT && (column.slotType === undefined || column.field === undefined),
|
|
184
|
+
'border': column.type !== GridColumnType.SLOT || (column.slotType !== undefined && column.field !== undefined)
|
|
185
|
+
}">
|
|
186
|
+
<div v-if="column.type !== GridColumnType.SLOT || (column.slotType !== undefined && column.field !== undefined)"
|
|
187
|
+
class="border-grey-2 border-r capitalize group inline relative w-auto">
|
|
188
|
+
<div class="bg-off-white flex h-full items-center justify-center w-10">
|
|
189
|
+
<icon icon="ph:sliders-horizontal"
|
|
190
|
+
:size="20" />
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="absolute bg-white flex-wrap font-normal group-hover:flex group-hover:h-auto group-hover:w-52 h-0 hidden pb-2 shadow-lg text-left text-sm top-10 z-10"
|
|
194
|
+
:class="{ 'left-0': firstHalf, '-left-44': !firstHalf }">
|
|
195
|
+
<strong class="p-3">
|
|
196
|
+
{{ localisations.currentMethod }}
|
|
197
|
+
</strong>
|
|
198
|
+
|
|
199
|
+
<span class="px-4 py-1 w-full">
|
|
200
|
+
{{ currentFilterMethod?.description }}
|
|
201
|
+
|
|
202
|
+
<span class="float-right"
|
|
203
|
+
v-html="currentFilterMethod?.icon"></span>
|
|
204
|
+
</span>
|
|
205
|
+
|
|
206
|
+
<strong class="p-3">
|
|
207
|
+
{{ localisations.availableMethods }}
|
|
208
|
+
</strong>
|
|
209
|
+
|
|
210
|
+
<span v-for="(filterMethod, index) in allowedFilterMethods"
|
|
211
|
+
:key="index"
|
|
212
|
+
class="cursor-pointer hover:bg-off-white px-4 py-1 w-full"
|
|
213
|
+
@click.prevent="setFilterMethod(filterMethod.method)">
|
|
214
|
+
{{ filterMethod.description }}
|
|
215
|
+
|
|
216
|
+
<span class="float-right"
|
|
217
|
+
v-html="filterMethod.icon"></span>
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- Booleans -->
|
|
223
|
+
<select v-if="column.type === GridColumnType.BOOLEAN ||
|
|
224
|
+
(column.slotType === GridColumnType.BOOLEAN && typeof column.field !== 'undefined')"
|
|
225
|
+
class="border-none w-full"
|
|
226
|
+
@change="setFilter($event.target)">
|
|
227
|
+
<option :value="undefined"></option>
|
|
228
|
+
|
|
229
|
+
<option value="true"
|
|
230
|
+
:selected="currentFilter === 'true'">
|
|
231
|
+
{{ localisations.true }}
|
|
232
|
+
</option>
|
|
233
|
+
|
|
234
|
+
<option value="false"
|
|
235
|
+
:selected="currentFilter === 'false'">
|
|
236
|
+
{{ localisations.false }}
|
|
237
|
+
</option>
|
|
238
|
+
</select>
|
|
239
|
+
<!-- Dates & DateTimes -->
|
|
240
|
+
<cl-ui-calendar v-else-if="column.type === GridColumnType.DATE || column.type === GridColumnType.DATETIME ||
|
|
241
|
+
((column.slotType === GridColumnType.DATE || column.slotType === GridColumnType.DATETIME) && typeof column.field !== 'undefined')"
|
|
242
|
+
:date="currentDateFilter"
|
|
243
|
+
class="border-none w-full"
|
|
244
|
+
:type="column.type"
|
|
245
|
+
:date-placeholder="localisations.selectDate"
|
|
246
|
+
:date-time-placeholder="localisations.selectDateTime"
|
|
247
|
+
:current-locale="currentLocale"
|
|
248
|
+
@update:date="setDateFilter($event)" />
|
|
249
|
+
<!-- Numbers -->
|
|
250
|
+
<input v-else-if="column.type === GridColumnType.NUMBER ||
|
|
251
|
+
(column.slotType === GridColumnType.NUMBER && typeof column.field !== 'undefined')"
|
|
252
|
+
:value="currentFilter"
|
|
253
|
+
class="border-none w-full"
|
|
254
|
+
type="text"
|
|
255
|
+
@keyup.arrow-up="setFilter(null, (currentNumberFilter + 1).toString())"
|
|
256
|
+
@keyup.arrow-down="setFilter(null, (currentNumberFilter - 1).toString())"
|
|
257
|
+
@input="debounce(setNumberFilter, [ $event.target ])">
|
|
258
|
+
<!-- Strings -->
|
|
259
|
+
<input v-else-if="column.type !== GridColumnType.SLOT ||
|
|
260
|
+
(column.slotType === GridColumnType.STRING && typeof column.field !== 'undefined')"
|
|
261
|
+
:value="currentFilter"
|
|
262
|
+
class="border-none w-full"
|
|
263
|
+
type="text"
|
|
264
|
+
@input="debounce(setFilter, [ $event.target ])">
|
|
265
|
+
</div>
|
|
266
|
+
<div v-else
|
|
267
|
+
class="flex w-full">
|
|
268
|
+
|
|
269
|
+
</div>
|
|
270
|
+
</template>
|
|
@@ -1,4 +1,95 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
4
|
+
|
|
5
|
+
import { FilterRequest, FilterResponse, GridFooterLocalisations, NumberFormat } from '../../../@types';
|
|
6
|
+
import { copy } from '../../../utils/copy';
|
|
7
|
+
import { useDebouncer } from '../../../utils/debouncer';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
request: FilterRequest;
|
|
11
|
+
currentLocale?: string;
|
|
12
|
+
localisations: GridFooterLocalisations;
|
|
13
|
+
data?: FilterResponse<Record<string, unknown>> | null;
|
|
14
|
+
}>(), {
|
|
15
|
+
currentLocale: 'en-GB',
|
|
16
|
+
data: () => null
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits({
|
|
20
|
+
'update:request': null
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const pageSizes = [
|
|
24
|
+
10,
|
|
25
|
+
20,
|
|
26
|
+
50,
|
|
27
|
+
100,
|
|
28
|
+
200
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const { n } = useI18n();
|
|
32
|
+
const { debounce } = useDebouncer();
|
|
33
|
+
|
|
34
|
+
const totalPages = computed<number>(() => props.data !== null ? Math.ceil(props.data.totalRecords / props.request.pageSize) : 1);
|
|
35
|
+
const pageNumbers = computed<number[]>(() => {
|
|
36
|
+
const calculatedPageNumbers: number[] = Array.from(Array(totalPages.value + 1).keys());
|
|
37
|
+
let results: number[] = [];
|
|
38
|
+
|
|
39
|
+
calculatedPageNumbers.shift();
|
|
40
|
+
|
|
41
|
+
if (calculatedPageNumbers.length) {
|
|
42
|
+
const startPage = (props.request.pageNumber === totalPages.value || props.request.pageNumber === totalPages.value - 1) ?
|
|
43
|
+
totalPages.value - 3 :
|
|
44
|
+
props.request.pageNumber - 1;
|
|
45
|
+
const endPage = startPage + 3;
|
|
46
|
+
|
|
47
|
+
results = calculatedPageNumbers.slice(startPage, endPage);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sets the page to the selected number.
|
|
55
|
+
*
|
|
56
|
+
* @param pageNumber The page to navigate to.
|
|
57
|
+
*/
|
|
58
|
+
function setPage (pageNumber: number): void {
|
|
59
|
+
const filterRequest = copy(props.request);
|
|
60
|
+
|
|
61
|
+
if (filterRequest.pageNumber !== pageNumber) {
|
|
62
|
+
filterRequest.pageNumber = Math.max(Math.min(Math.trunc(pageNumber), totalPages.value), 1);
|
|
63
|
+
|
|
64
|
+
emit('update:request', filterRequest);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets the page to the value in the provided input.
|
|
70
|
+
*
|
|
71
|
+
* @param target The input element.
|
|
72
|
+
*/
|
|
73
|
+
function setPageFromInput (target: EventTarget | null): void {
|
|
74
|
+
const pageNumber: number = parseInt((target as HTMLInputElement)?.value) || 1;
|
|
75
|
+
|
|
76
|
+
setPage(pageNumber);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sets the page size to the selected number.
|
|
81
|
+
*
|
|
82
|
+
* @param pageSize The page size to use.
|
|
83
|
+
*/
|
|
84
|
+
function setPageSize (pageSize: number): void {
|
|
85
|
+
const filterRequest = copy(props.request);
|
|
86
|
+
|
|
87
|
+
filterRequest.pageSize = pageSize;
|
|
88
|
+
filterRequest.pageNumber = 1;
|
|
89
|
+
|
|
90
|
+
emit('update:request', filterRequest);
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
2
93
|
|
|
3
94
|
<template>
|
|
4
95
|
<div v-if="data && data.results?.length && request"
|
|
@@ -29,13 +120,15 @@
|
|
|
29
120
|
<li v-show="request.pageNumber > 1"
|
|
30
121
|
class="cursor-pointer lg:mr-2 mr-1 mt-1"
|
|
31
122
|
@click="setPage(1)">
|
|
32
|
-
<ph
|
|
123
|
+
<icon icon="ph:caret-double-left"
|
|
124
|
+
:size="14" />
|
|
33
125
|
</li>
|
|
34
126
|
|
|
35
127
|
<li v-show="request.pageNumber > 1"
|
|
36
128
|
class="cursor-pointer lg:mr-2 mr-1 mt-1"
|
|
37
129
|
@click="setPage(request.pageNumber - 1)">
|
|
38
|
-
<ph
|
|
130
|
+
<icon icon="ph:caret-left"
|
|
131
|
+
:size="14" />
|
|
39
132
|
</li>
|
|
40
133
|
|
|
41
134
|
<li v-for="(number, index) in pageNumbers"
|
|
@@ -52,13 +145,15 @@
|
|
|
52
145
|
<li v-show="request.pageNumber < totalPages"
|
|
53
146
|
class="cursor-pointer lg:mr-2 mr-1 mt-1"
|
|
54
147
|
@click="setPage(request.pageNumber + 1)">
|
|
55
|
-
<ph
|
|
148
|
+
<icon icon="ph:caret-right"
|
|
149
|
+
:size="14" />
|
|
56
150
|
</li>
|
|
57
151
|
|
|
58
152
|
<li v-show="request.pageNumber < totalPages"
|
|
59
153
|
class="cursor-pointer lg:mr-2 mr-1 mt-1"
|
|
60
154
|
@click="setPage(totalPages)">
|
|
61
|
-
<ph
|
|
155
|
+
<icon icon="ph:caret-double-right"
|
|
156
|
+
:size="14" />
|
|
62
157
|
</li>
|
|
63
158
|
</ul>
|
|
64
159
|
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { GridColumn, GridHeaderLocalisations, GridViewManagerLocalisations } from '../../../@types';
|
|
5
|
+
import ClUiGridViewManager from '../view-manager/cl-ui-grid-view-manager.vue';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
columns: GridColumn[];
|
|
9
|
+
editMode: boolean;
|
|
10
|
+
filterPanelOpen?: boolean;
|
|
11
|
+
localisations?: GridHeaderLocalisations & GridViewManagerLocalisations;
|
|
12
|
+
}>(), {
|
|
13
|
+
filterPanelOpen: false,
|
|
14
|
+
localisations: () => ({
|
|
15
|
+
editData: 'Edit Data',
|
|
16
|
+
finishEditing: 'Finish Editing',
|
|
17
|
+
clearFilters: 'Clear Filters',
|
|
18
|
+
manageView: 'Manage View',
|
|
19
|
+
modifyFilters: 'Modify Filters',
|
|
20
|
+
column: 'Column',
|
|
21
|
+
visible: 'Visible',
|
|
22
|
+
order: 'Order',
|
|
23
|
+
hidden: 'Hidden'
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits({
|
|
28
|
+
'reset-filters': null,
|
|
29
|
+
'update:columns': null,
|
|
30
|
+
'update:edit-mode': null,
|
|
31
|
+
'update:filter-panel-open': null
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const currentColumns = computed<GridColumn[]>({
|
|
35
|
+
get: () => props.columns,
|
|
36
|
+
set: (value) => emit('update:columns', value)
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<span class="lg:pb-0 lg:w-auto pb-2 pr-2 w-1/2">
|
|
42
|
+
<cl-ui-button class="lg:w-auto w-full"
|
|
43
|
+
size="small"
|
|
44
|
+
colour="default"
|
|
45
|
+
@click="$emit('update:edit-mode', !editMode)">
|
|
46
|
+
<span v-show="!editMode">
|
|
47
|
+
{{ localisations.editData }}
|
|
48
|
+
</span>
|
|
49
|
+
<span v-show="editMode">
|
|
50
|
+
{{ localisations.finishEditing }}
|
|
51
|
+
</span>
|
|
52
|
+
</cl-ui-button>
|
|
53
|
+
</span>
|
|
54
|
+
|
|
55
|
+
<span class="hidden lg:inline-block pr-2">
|
|
56
|
+
<cl-ui-grid-view-manager v-model:columns="currentColumns"
|
|
57
|
+
:localisations="localisations" />
|
|
58
|
+
</span>
|
|
59
|
+
|
|
60
|
+
<span class="lg:pb-0 lg:pr-0 lg:w-auto pb-2 pr-2 w-1/2">
|
|
61
|
+
<cl-ui-button class="lg:w-auto w-full"
|
|
62
|
+
size="small"
|
|
63
|
+
@click.prevent="$emit('reset-filters')">
|
|
64
|
+
{{ localisations.clearFilters }}
|
|
65
|
+
</cl-ui-button>
|
|
66
|
+
</span>
|
|
67
|
+
|
|
68
|
+
<span class="lg:hidden pr-2 w-1/2">
|
|
69
|
+
<cl-ui-button class="w-full"
|
|
70
|
+
colour="blue"
|
|
71
|
+
size="small"
|
|
72
|
+
@click.prevent="$emit('update:filter-panel-open', true)">
|
|
73
|
+
{{ localisations.modifyFilters }}
|
|
74
|
+
</cl-ui-button>
|
|
75
|
+
</span>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default {
|
|
3
|
+
inheritAttrs: false
|
|
4
|
+
};
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { ref } from 'vue';
|
|
9
|
+
|
|
10
|
+
import { GridColumn, GridViewManagerLocalisations } from '../../../@types';
|
|
11
|
+
import { copy } from '../../../utils/copy';
|
|
12
|
+
import ClUiButton from '../../button/cl-ui-button.vue';
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
localisations: GridViewManagerLocalisations;
|
|
16
|
+
columns: GridColumn[];
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits({
|
|
20
|
+
'update:columns': null
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const visible = ref<boolean>(false);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Moves the position of a column, in the list of columns.
|
|
27
|
+
*
|
|
28
|
+
* @param columns List of columns to update.
|
|
29
|
+
* @param oldIndex Current index of the column being updated.
|
|
30
|
+
* @param newIndex New index of the column being updated.
|
|
31
|
+
*/
|
|
32
|
+
function moveColumn (columns: GridColumn[], oldIndex: number, newIndex: number): void {
|
|
33
|
+
if (newIndex < columns.length && newIndex >= 0) {
|
|
34
|
+
columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Updates a column to either hide or show it, on the grid.
|
|
40
|
+
*
|
|
41
|
+
* @param target The selected checkbox.
|
|
42
|
+
* @param column The column to update.
|
|
43
|
+
*/
|
|
44
|
+
function updateColumnVisibility (target: EventTarget | null, column: GridColumn): void {
|
|
45
|
+
const gridColumns = copy(props.columns);
|
|
46
|
+
const checked = (target as HTMLInputElement | null)?.checked ?? false;
|
|
47
|
+
const targetColumnIndex: number = gridColumns.findIndex(c => c.name === column.name);
|
|
48
|
+
|
|
49
|
+
if (targetColumnIndex >= 0) {
|
|
50
|
+
gridColumns[targetColumnIndex].visible = checked;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
emit('update:columns', gridColumns);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Moves the column, at the specified index, either one position further up or down, in the current column list.
|
|
58
|
+
*
|
|
59
|
+
* @param columnIndex Index of the column to update.
|
|
60
|
+
* @param direction Whether the column is being moved up or down.
|
|
61
|
+
*/
|
|
62
|
+
function changeColumnOrder (columnIndex: number, direction: 'up' | 'down'): void {
|
|
63
|
+
const gridColumns = copy(props.columns);
|
|
64
|
+
const newIndex: number = direction === 'up' ? columnIndex + 1 : columnIndex - 1;
|
|
65
|
+
|
|
66
|
+
moveColumn(gridColumns, columnIndex, newIndex);
|
|
67
|
+
|
|
68
|
+
emit('update:columns', gridColumns);
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<cl-ui-button v-bind="$attrs"
|
|
74
|
+
class="w-full"
|
|
75
|
+
size="small"
|
|
76
|
+
@click.prevent="visible = true">
|
|
77
|
+
{{ localisations.manageView }}
|
|
78
|
+
</cl-ui-button>
|
|
79
|
+
|
|
80
|
+
<teleport to="body">
|
|
81
|
+
<div v-show="visible"
|
|
82
|
+
class="fixed flex h-full right-0 top-0 w-full z-40">
|
|
83
|
+
<div class="bg-black cursor-pointer flex-1 h-full opacity-40"
|
|
84
|
+
@click="visible = false"></div>
|
|
85
|
+
|
|
86
|
+
<div class="bg-secondary-default h-full overflow-y-auto px-10 py-20 relative text-grey-2 w-80">
|
|
87
|
+
<icon class="absolute cursor-pointer right-3 text-off-white top-3"
|
|
88
|
+
icon="ph:x"
|
|
89
|
+
:size="16"
|
|
90
|
+
@click="visible = false" />
|
|
91
|
+
|
|
92
|
+
<h3 class="mb-4 text-2xl text-off-white">
|
|
93
|
+
{{ localisations.manageView }}
|
|
94
|
+
</h3>
|
|
95
|
+
|
|
96
|
+
<ul class="border-grey-3 border-t flex flex-wrap text-sm w-full">
|
|
97
|
+
<li class="border-b border-grey-2 flex py-2 text-sm w-full">
|
|
98
|
+
<strong class="flex-1 py-2">
|
|
99
|
+
{{ localisations.column }}
|
|
100
|
+
</strong>
|
|
101
|
+
|
|
102
|
+
<strong class="py-2 w-1/4">
|
|
103
|
+
{{ localisations.visible }}
|
|
104
|
+
</strong>
|
|
105
|
+
|
|
106
|
+
<strong class="py-2 w-1/5">
|
|
107
|
+
{{ localisations.order }}
|
|
108
|
+
</strong>
|
|
109
|
+
</li>
|
|
110
|
+
|
|
111
|
+
<li v-for="(column, index) in columns"
|
|
112
|
+
:key="index"
|
|
113
|
+
class="border-b border-grey-2 flex py-2 text-sm w-full">
|
|
114
|
+
<div class="flex-1 py-2">
|
|
115
|
+
{{ column.caption }}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="py-2 w-1/4">
|
|
119
|
+
<input :checked="column.visible === undefined || column.visible === true"
|
|
120
|
+
type="checkbox"
|
|
121
|
+
@click="updateColumnVisibility($event.target, column)">
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="py-2 text-off-white w-1/5">
|
|
125
|
+
<template v-if="column.visible === undefined || column.visible === true">
|
|
126
|
+
<icon v-if="index !== 0"
|
|
127
|
+
class="cursor-pointer inline-block mr-2"
|
|
128
|
+
icon="ph:arrow-left"
|
|
129
|
+
@click="changeColumnOrder(index, 'down')" />
|
|
130
|
+
|
|
131
|
+
<icon v-if="index !== columns.length - 1"
|
|
132
|
+
class="cursor-pointer inline-block"
|
|
133
|
+
icon="ph:arrow-right"
|
|
134
|
+
@click="changeColumnOrder(index, 'up')" />
|
|
135
|
+
</template>
|
|
136
|
+
<em v-else>
|
|
137
|
+
{{ localisations.hidden }}
|
|
138
|
+
</em>
|
|
139
|
+
</div>
|
|
140
|
+
</li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</teleport>
|
|
145
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header class="absolute bg-secondary-default flex h-20 items-center justify-between left-0 top-0 w-full z-40">
|
|
3
|
+
<slot name="logo"></slot>
|
|
4
|
+
|
|
5
|
+
<div class="flex flex-nowrap">
|
|
6
|
+
<slot name="dropdown"></slot>
|
|
7
|
+
|
|
8
|
+
<slot name="iconTool"></slot>
|
|
9
|
+
</div>
|
|
10
|
+
</header>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { AppUser } from '../../@types';
|
|
5
|
+
|
|
6
|
+
withDefaults(defineProps<{
|
|
7
|
+
user?: AppUser | null;
|
|
8
|
+
}>(), {
|
|
9
|
+
user: () => null
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const isOpen = ref<boolean>(false);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="md:relative">
|
|
17
|
+
<div class="bg-blue-light flex items-center md:min-w-[320px] select-none">
|
|
18
|
+
<template v-if="user">
|
|
19
|
+
<img v-if="user.userImage"
|
|
20
|
+
:src="user.userImage"
|
|
21
|
+
class="hidden m-4 max-h-12 max-w-12 md:block ring-2 ring-white rounded-full">
|
|
22
|
+
|
|
23
|
+
<div v-if="user.userName || (user.userGroup && user.userGroup.groupName)"
|
|
24
|
+
class="cursor-default flex-grow hidden md:block my-4 text-white">
|
|
25
|
+
<div v-if="user.userName"
|
|
26
|
+
class="cursor-default overflow-ellipsis whitespace-nowrap">
|
|
27
|
+
{{ user.userName }}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div v-if="user.userGroup && user.userGroup.groupName"
|
|
31
|
+
class="cursor-default overflow-ellipsis text-xs whitespace-nowrap">
|
|
32
|
+
{{ user.userGroup.groupName }}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="bg-transparent content-center cursor-pointer flex h-20 hover:bg-opacity-10 hover:bg-white items-center justify-center justify-items-center md:ml-4 md:w-20 transition-colors w-10"
|
|
37
|
+
@click="isOpen = !isOpen">
|
|
38
|
+
<icon class="text-white transform transition-transform"
|
|
39
|
+
:class="{ 'rotate-180' : isOpen }"
|
|
40
|
+
icon="ph:caret-down" />
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div v-if="isOpen"
|
|
46
|
+
class="absolute bg-white right-0 shadow-2xl top-20 w-full z-20">
|
|
47
|
+
<slot name="content"></slot>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|