quasar-ui-danx 0.4.2 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. package/dist/danx.es.js +7127 -6615
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +11 -5
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -1
  7. package/src/components/ActionTable/ActionTable.vue +28 -41
  8. package/src/components/ActionTable/Columns/ActionTableColumn.vue +19 -18
  9. package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +6 -6
  10. package/src/components/ActionTable/Filters/{FilterFieldList.vue → FilterList.vue} +26 -26
  11. package/src/components/ActionTable/Filters/FilterableField.vue +28 -31
  12. package/src/components/ActionTable/Filters/index.ts +2 -2
  13. package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +71 -0
  14. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +8 -13
  15. package/src/components/ActionTable/Form/Fields/MultiFileField.vue +48 -44
  16. package/src/components/ActionTable/Form/Fields/SelectField.vue +24 -38
  17. package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +28 -33
  18. package/src/components/ActionTable/Form/Fields/SingleFileField.vue +15 -15
  19. package/src/components/ActionTable/Form/Fields/SliderNumberField.vue +45 -0
  20. package/src/components/ActionTable/Form/Fields/TextField.vue +47 -66
  21. package/src/components/ActionTable/Form/Fields/index.ts +2 -0
  22. package/src/components/ActionTable/Form/RenderedForm.vue +50 -9
  23. package/src/components/ActionTable/Form/Utilities/MaxLengthCounter.vue +17 -0
  24. package/src/components/ActionTable/Form/Utilities/index.ts +1 -0
  25. package/src/components/ActionTable/Form/index.ts +1 -0
  26. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +16 -15
  27. package/src/components/ActionTable/Toolbars/ActionToolbar.vue +6 -6
  28. package/src/components/ActionTable/listControls.ts +106 -166
  29. package/src/components/ActionTable/listHelpers.ts +2 -3
  30. package/src/components/ActionTable/tableColumns.ts +3 -27
  31. package/src/components/AuditHistory/AuditHistoryItemValue.vue +26 -26
  32. package/src/components/PanelsDrawer/PanelsDrawer.vue +17 -4
  33. package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +6 -11
  34. package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +20 -20
  35. package/src/components/Utility/Dialogs/ConfirmActionDialog.vue +39 -0
  36. package/src/components/Utility/Dialogs/ConfirmDialog.vue +10 -24
  37. package/src/components/Utility/Dialogs/DialogLayout.vue +10 -28
  38. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +42 -36
  39. package/src/components/Utility/Dialogs/index.ts +1 -0
  40. package/src/components/Utility/Files/FilePreview.vue +76 -73
  41. package/src/components/Utility/Layouts/ContentDrawer.vue +24 -31
  42. package/src/components/Utility/Tools/ActionVnode.vue +3 -3
  43. package/src/components/Utility/Tools/RenderVnode.vue +1 -1
  44. package/src/components/Utility/Transitions/MaxHeightTransition.vue +26 -0
  45. package/src/components/Utility/Transitions/index.ts +1 -0
  46. package/src/config/index.ts +36 -31
  47. package/src/helpers/FileUpload.ts +295 -297
  48. package/src/helpers/FlashMessages.ts +80 -71
  49. package/src/helpers/actions.ts +102 -82
  50. package/src/helpers/download.ts +189 -189
  51. package/src/helpers/downloadPdf.ts +55 -52
  52. package/src/helpers/formats.ts +151 -109
  53. package/src/helpers/index.ts +2 -0
  54. package/src/helpers/multiFileUpload.ts +72 -58
  55. package/src/helpers/objectStore.ts +52 -0
  56. package/src/helpers/request.ts +70 -51
  57. package/src/helpers/routes.ts +29 -0
  58. package/src/helpers/storage.ts +7 -3
  59. package/src/helpers/utils.ts +47 -29
  60. package/src/styles/quasar-reset.scss +16 -1
  61. package/src/styles/themes/danx/dialogs.scss +4 -0
  62. package/src/types/actions.d.ts +43 -0
  63. package/src/types/config.d.ts +15 -0
  64. package/src/types/controls.d.ts +99 -0
  65. package/src/types/dialogs.d.ts +32 -0
  66. package/src/types/fields.d.ts +20 -0
  67. package/src/types/files.d.ts +54 -0
  68. package/src/types/formats.d.ts +4 -0
  69. package/src/{components/ActionTable/Form/form.d.ts → types/forms.d.ts} +6 -0
  70. package/src/types/index.d.ts +12 -0
  71. package/src/types/requests.d.ts +13 -0
  72. package/src/types/shared.d.ts +15 -0
  73. package/src/types/tables.d.ts +27 -0
  74. package/types/index.d.ts +1 -1
  75. /package/src/components/ActionTable/Filters/{FilterFieldItem.vue → FilterItem.vue} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@quasar/extras": "^1.16.4",
22
+ "@types/luxon": "^3.4.2",
22
23
  "@types/node": "^20.12.7",
23
24
  "@typescript-eslint/eslint-plugin": "^7.6.0",
24
25
  "@typescript-eslint/parser": "^7.6.0",
@@ -48,6 +49,7 @@
48
49
  "@heroicons/vue": "v1",
49
50
  "@tinymce/tinymce-vue": "^5.1.1",
50
51
  "@vueuse/core": "^10.7.2",
52
+ "danx-icon": "^1.0.2",
51
53
  "exifreader": "^4.21.1",
52
54
  "gsap": "^3.12.5",
53
55
  "luxon": "^3.4.4"
@@ -62,10 +62,11 @@
62
62
  </div>
63
63
  </template>
64
64
 
65
- <script setup>
65
+ <script setup lang="ts">
66
66
  import { QTable } from "quasar";
67
67
  import { computed, ref } from "vue";
68
68
  import { getItem, setItem } from "../../helpers";
69
+ import { ActionTargetItem, ListControlsPagination, TableColumn } from "../../types";
69
70
  import { ActionVnode } from "../Utility";
70
71
  import { ActionTableColumn, ActionTableHeaderColumn } from "./Columns";
71
72
  import EmptyTableState from "./EmptyTableState.vue";
@@ -73,50 +74,36 @@ import { mapSortBy, registerStickyScrolling } from "./listHelpers";
73
74
  import TableSummaryRow from "./TableSummaryRow.vue";
74
75
 
75
76
  defineEmits(["update:selected-rows", "update:pagination"]);
76
- const props = defineProps({
77
- name: {
78
- type: String,
79
- required: true
80
- },
81
- label: {
82
- type: String,
83
- required: true
84
- },
85
- color: {
86
- type: String,
87
- default: "blue-600"
88
- },
89
- selectedRows: {
90
- type: Array,
91
- required: true
92
- },
93
- pagination: {
94
- type: Object,
95
- required: true
96
- },
97
- loadingList: Boolean,
98
- loadingSummary: Boolean,
99
- pagedItems: {
100
- type: Object,
101
- default: null
102
- },
103
- summary: {
104
- type: Object,
105
- default: null
106
- },
107
- columns: {
108
- type: Array,
109
- required: true
110
- },
111
- rowsPerPageOptions: {
112
- type: Array,
113
- default: () => [10, 25, 50, 100]
114
- }
77
+
78
+ export interface Props {
79
+ name: string;
80
+ label: string;
81
+ color?: string;
82
+ selectedRows: ActionTargetItem[];
83
+ pagination: ListControlsPagination;
84
+ loadingList?: boolean;
85
+ loadingSummary?: boolean;
86
+ pagedItems?: any;
87
+ summary: any;
88
+ columns: TableColumn[];
89
+ rowsPerPageOptions?: number[];
90
+ }
91
+
92
+ const props = withDefaults(defineProps<Props>(), {
93
+ color: "",
94
+ pagedItems: null,
95
+ summary: null,
96
+ loadingSummary: false,
97
+ rowsPerPageOptions: () => [10, 25, 50, 100]
115
98
  });
99
+
116
100
  const actionTable = ref(null);
117
101
  registerStickyScrolling(actionTable);
118
102
 
119
- const tableColumns = computed(() => props.columns.map((column) => ({ ...column, field: column.field || column.name })));
103
+ const tableColumns = computed<TableColumn[]>(() => props.columns.map((column) => ({
104
+ ...column,
105
+ field: column.field || column.name
106
+ })));
120
107
  const hasData = computed(() => props.pagedItems?.data?.length);
121
108
  const COLUMN_SETTINGS_KEY = `column-settings-${props.name}`;
122
109
  const columnSettings = ref(getItem(COLUMN_SETTINGS_KEY) || {});
@@ -13,6 +13,7 @@
13
13
  <a
14
14
  v-if="column.onClick"
15
15
  :class="column.innerClass"
16
+ class="dx-column-link"
16
17
  @click="column.onClick(row)"
17
18
  >
18
19
  <RenderVnode
@@ -62,14 +63,14 @@ import ActionMenu from "../ActionMenu";
62
63
  import { TitleColumnFormat } from "./";
63
64
 
64
65
  const props = defineProps({
65
- rowProps: {
66
- type: Object,
67
- required: true
68
- },
69
- settings: {
70
- type: Object,
71
- default: null
72
- }
66
+ rowProps: {
67
+ type: Object,
68
+ required: true
69
+ },
70
+ settings: {
71
+ type: Object,
72
+ default: null
73
+ }
73
74
  });
74
75
 
75
76
  const row = computed(() => props.rowProps.row);
@@ -78,18 +79,18 @@ const value = computed(() => props.rowProps.value);
78
79
  const isSaving = computed(() => row.value.isSaving?.value);
79
80
 
80
81
  const columnStyle = computed(() => {
81
- const width = props.settings?.width || column.value.width;
82
- return {
83
- width: width ? `${width}px` : undefined,
84
- minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined
85
- };
82
+ const width = props.settings?.width || column.value.width;
83
+ return {
84
+ width: width ? `${width}px` : undefined,
85
+ minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined
86
+ };
86
87
  });
87
88
 
88
89
  const columnClass = computed(() => ({
89
- [column.value.class || ""]: true,
90
- "is-saving": isSaving.value,
91
- "justify-end": column.value.align === "right",
92
- "justify-center": column.value.align === "center",
93
- "justify-start": column.value.align === "left"
90
+ [column.value.class || ""]: true,
91
+ "is-saving": isSaving.value,
92
+ "justify-end": column.value.align === "right",
93
+ "justify-center": column.value.align === "center",
94
+ "justify-start": column.value.align === "left"
94
95
  }));
95
96
  </script>
@@ -8,17 +8,17 @@
8
8
  :name="name"
9
9
  @update:collapse="$emit('update:show-filters', !$event)"
10
10
  >
11
- <FilterFieldList
12
- :filter="activeFilter"
13
- :filter-fields="filters"
11
+ <FilterList
12
+ :active-filter="activeFilter"
13
+ :filters="filters"
14
14
  @update:filter="$emit('update:active-filter', $event)"
15
15
  />
16
16
  </CollapsableSidebar>
17
17
  </template>
18
18
  <script setup lang="ts">
19
- import { FilterField, ListControlsFilter } from "src/components/ActionTable/listControls";
20
- import { FilterFieldList } from ".";
19
+ import { FilterGroup, ListControlsFilter } from "../../../types";
21
20
  import { CollapsableSidebar } from "../../Utility";
21
+ import FilterList from "./FilterList";
22
22
 
23
23
  defineEmits(["update:active-filter", "update:show-filters"]);
24
24
 
@@ -28,7 +28,7 @@ export interface Props {
28
28
  activeFilter: ListControlsFilter,
29
29
  minWidth?: string,
30
30
  maxWidth?: string,
31
- filters?: FilterField[]
31
+ filters?: FilterGroup[]
32
32
  }
33
33
 
34
34
  withDefaults(defineProps<Props>(), {
@@ -2,14 +2,14 @@
2
2
  <QList>
3
3
  <div class="px-4 py-2 max-w-full">
4
4
  <template
5
- v-for="(group, index) in filterFields"
5
+ v-for="(group, index) in filters"
6
6
  :key="'group-' + group.name"
7
7
  >
8
8
  <template v-if="group.flat">
9
9
  <FilterableField
10
10
  v-for="field in group.fields"
11
11
  :key="'field-' + field.name"
12
- :model-value="field.calcValue ? field.calcValue(filter) : filter[field.name]"
12
+ :model-value="field.calcValue ? field.calcValue(activeFilter) : activeFilter[field.name]"
13
13
  :field="field"
14
14
  :loading="loading"
15
15
  class="mb-4"
@@ -17,7 +17,7 @@
17
17
  />
18
18
  </template>
19
19
 
20
- <FilterFieldItem
20
+ <FilterItem
21
21
  v-else
22
22
  :name="group.name"
23
23
  :count="activeCountByGroup[group.name]"
@@ -25,16 +25,16 @@
25
25
  <FilterableField
26
26
  v-for="field in group.fields"
27
27
  :key="'field-' + field.name"
28
- :model-value="field.calcValue ? field.calcValue(filter) : filter[field.name]"
28
+ :model-value="field.calcValue ? field.calcValue(activeFilter) : activeFilter[field.name]"
29
29
  :field="field"
30
30
  :loading="loading"
31
31
  class="mb-4"
32
32
  @update:model-value="updateFilter(field, $event)"
33
33
  />
34
- </FilterFieldItem>
34
+ </FilterItem>
35
35
 
36
36
  <QSeparator
37
- v-if="index < (filterFields.length - 1)"
37
+ v-if="index < (filters.length - 1)"
38
38
  class="my-2"
39
39
  />
40
40
  </template>
@@ -44,33 +44,33 @@
44
44
  <script setup>
45
45
  import { computed } from "vue";
46
46
  import FilterableField from "./FilterableField";
47
- import FilterFieldItem from "./FilterFieldItem";
47
+ import FilterItem from "./FilterItem";
48
48
 
49
49
  const emit = defineEmits(["update:filter"]);
50
50
  const props = defineProps({
51
- filterFields: {
52
- type: Array,
53
- required: true
54
- },
55
- filter: {
56
- type: Object,
57
- required: true
58
- },
59
- loading: Boolean
51
+ filters: {
52
+ type: Array,
53
+ required: true
54
+ },
55
+ activeFilter: {
56
+ type: Object,
57
+ required: true
58
+ },
59
+ loading: Boolean
60
60
  });
61
61
 
62
62
  const activeCountByGroup = computed(() => {
63
- const activeCountByGroup = {};
64
- for (const group of props.filterFields) {
65
- activeCountByGroup[group.name] = group.fields.filter(field => props.filter[field.name] !== undefined).length;
66
- }
67
- return activeCountByGroup;
63
+ const activeCountByGroup = {};
64
+ for (const group of props.filters) {
65
+ activeCountByGroup[group.name] = group.fields.filter(field => props.activeFilter[field.name] !== undefined).length;
66
+ }
67
+ return activeCountByGroup;
68
68
  });
69
69
  function updateFilter(field, value) {
70
- let fieldFilter = { [field.name]: value };
71
- if (field.filterBy) {
72
- fieldFilter = field.filterBy(value);
73
- }
74
- emit("update:filter", { ...props.filter, ...fieldFilter });
70
+ let activeFilter = { [field.name]: value };
71
+ if (field.filterBy) {
72
+ activeFilter = field.filterBy(value);
73
+ }
74
+ emit("update:filter", { ...props.activeFilter, ...activeFilter });
75
75
  }
76
76
  </script>
@@ -5,6 +5,7 @@
5
5
  v-if="field.options?.length > 0 || loading"
6
6
  :model-value="modelValue"
7
7
  :options="field.options"
8
+ :clearable="field.clearable === undefined ? true : field.clearable"
8
9
  multiple
9
10
  :loading="loading"
10
11
  :chip-limit="1"
@@ -30,6 +31,7 @@
30
31
  v-else-if="field.type === 'single-select'"
31
32
  :model-value="modelValue"
32
33
  :options="field.options"
34
+ :clearable="field.clearable === undefined ? true : field.clearable"
33
35
  :placeholder="field.placeholder"
34
36
  :loading="loading"
35
37
  :label="field.label"
@@ -46,7 +48,7 @@
46
48
  v-else-if="field.type === 'date-range'"
47
49
  :model-value="modelValue"
48
50
  :label="field.label"
49
- :inline="field.inline"
51
+ :inline="!!field.inline"
50
52
  with-time
51
53
  class="mt-2 reactive"
52
54
  @update:model-value="onUpdate"
@@ -106,43 +108,38 @@
106
108
  </template>
107
109
  </div>
108
110
  </template>
109
- <script setup>
111
+ <script setup lang="ts">
112
+ import { FormField } from "../../../types";
110
113
  import {
111
- BooleanField,
112
- DateField,
113
- DateRangeField,
114
- MultiKeywordField,
115
- NumberRangeField,
116
- SelectField,
117
- SelectWithChildrenField
114
+ BooleanField,
115
+ DateField,
116
+ DateRangeField,
117
+ MultiKeywordField,
118
+ NumberRangeField,
119
+ SelectField,
120
+ SelectWithChildrenField
118
121
  } from "../Form/Fields";
119
122
 
120
123
  const emit = defineEmits(["update:model-value"]);
121
- const props = defineProps({
122
- field: {
123
- type: Object,
124
- required: true
125
- },
126
- modelValue: {
127
- type: [String, Array, Number, Object, Boolean],
128
- default: undefined
129
- },
130
- loading: Boolean
131
- });
124
+ const props = defineProps<{
125
+ field: FormField;
126
+ modelValue?: any;
127
+ loading?: boolean;
128
+ }>();
132
129
 
133
130
  function onUpdate(val) {
134
- let newVal = val || undefined;
131
+ let newVal = val || undefined;
135
132
 
136
- switch (props.field.type) {
137
- case "multi-select":
138
- newVal = val.length > 0 ? val : undefined;
139
- break;
140
- case "single-select":
141
- case "boolean":
142
- newVal = val === null ? undefined : val;
143
- break;
144
- }
133
+ switch (props.field.type) {
134
+ case "multi-select":
135
+ newVal = (val && val.length > 0) ? val : undefined;
136
+ break;
137
+ case "single-select":
138
+ case "boolean":
139
+ newVal = val === null ? undefined : val;
140
+ break;
141
+ }
145
142
 
146
- emit("update:model-value", newVal);
143
+ emit("update:model-value", newVal);
147
144
  }
148
145
  </script>
@@ -1,6 +1,6 @@
1
1
  export { default as CollapsableFiltersSidebar } from "./CollapsableFiltersSidebar.vue";
2
2
  export { default as FilterableField } from "./FilterableField.vue";
3
- export { default as FilterFieldItem } from "./FilterFieldItem.vue";
4
- export { default as FilterFieldList } from "./FilterFieldList.vue";
3
+ export { default as FilterItem } from "./FilterItem.vue";
4
+ export { default as FilterList } from "./FilterList.vue";
5
5
  export { default as FilterListToggle } from "./FilterListToggle.vue";
6
6
  export { default as FilterToolbarLayout } from "./FilterToolbarLayout.vue";
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div
3
+ class="danx-edit-on-click-text-field flex flex-nowrap items-center rounded overflow-ellipsis"
4
+ :class="{[props.class]: true, 'is-readonly': readonly, 'cursor-pointer': !isEditing && !readonly}"
5
+ @click="onEdit"
6
+ >
7
+ <div
8
+ ref="editableBox"
9
+ :contenteditable="!readonly && isEditing"
10
+ class="flex-grow p-2 rounded outline-none border-none"
11
+ :class="{[editingClass]: isEditing}"
12
+ @input="text = $event.target.innerText"
13
+ >
14
+ {{ text }}
15
+ </div>
16
+ <div v-if="!readonly">
17
+ <QBtn
18
+ v-if="isEditing"
19
+ @click.stop="isEditing = false"
20
+ >
21
+ <DoneIcon class="w-4" />
22
+ </QBtn>
23
+ <QBtn
24
+ v-else
25
+ class="edit-icon"
26
+ >
27
+ <EditIcon class="w-4" />
28
+ </QBtn>
29
+ </div>
30
+ </div>
31
+ </template>
32
+ <script lang="ts" setup>
33
+ import { FaSolidCheck as DoneIcon, FaSolidPencil as EditIcon } from "danx-icon";
34
+ import { nextTick, ref } from "vue";
35
+
36
+ export interface Props {
37
+ class?: "hover:bg-slate-300",
38
+ editingClass?: "bg-slate-500";
39
+ readonly?: boolean;
40
+ }
41
+
42
+ const editableBox = ref<HTMLElement | null>(null);
43
+ const text = defineModel({ type: String });
44
+ const props = defineProps<Props>();
45
+ const isEditing = ref(false);
46
+ function onEdit() {
47
+ if (props.readonly) return;
48
+ isEditing.value = true;
49
+ nextTick(() => {
50
+ editableBox.value?.focus();
51
+ });
52
+ }
53
+
54
+ </script>
55
+
56
+ <style lang="scss" scoped>
57
+ .danx-edit-on-click-text-field {
58
+ @apply transition-all;
59
+
60
+ .edit-icon {
61
+ @apply opacity-0 transition-all;
62
+
63
+ }
64
+
65
+ &:hover {
66
+ .edit-icon {
67
+ @apply opacity-100;
68
+ }
69
+ }
70
+ }
71
+ </style>
@@ -11,21 +11,16 @@
11
11
  </span>
12
12
  </template>
13
13
 
14
- <script setup>
14
+ <script setup lang="ts">
15
+ import { FormField } from "src/types";
15
16
  import { computed } from "vue";
16
17
 
17
- const props = defineProps({
18
- field: {
19
- type: Object,
20
- default: null
21
- },
22
- label: {
23
- type: String,
24
- default: null
25
- },
26
- showName: Boolean,
27
- required: Boolean
28
- });
18
+ const props = defineProps<{
19
+ field?: FormField;
20
+ label?: string;
21
+ showName?: boolean;
22
+ required?: boolean;
23
+ }>();
29
24
 
30
25
  const labelText = computed(() => props.label || props.field?.label);
31
26
  const requiredLabel = computed(() => props.field?.required_group || (props.required || props.field?.required ? "*" : ""));
@@ -1,55 +1,62 @@
1
1
  <template>
2
2
  <div
3
3
  class="max-w-full relative overflow-auto"
4
- :class="{'p-4 border rounded border-gray-300': !readonly}"
4
+ :class="{'p-2': !readonly}"
5
5
  @dragover.prevent
6
6
  @drop.prevent="onDrop"
7
7
  >
8
8
  <FieldLabel
9
9
  :field="field"
10
+ :label="label"
10
11
  :show-name="showName"
11
12
  class="text-sm font-semibold"
12
13
  />
13
- <div
14
+
15
+ <input
14
16
  v-if="!disable && !readonly"
15
- class="text-sm my-2"
17
+ ref="file"
18
+ class="hidden"
19
+ type="file"
20
+ multiple
21
+ @change="onFilesSelected"
16
22
  >
17
- <a
18
- class="text-blue-600"
19
- @click="$refs.file.click()"
20
- >Upload</a>
21
- <a
22
- v-if="uploadedFiles.length > 0"
23
- class="ml-3 text-red-900"
24
- @click="clearUploadedFiles"
25
- >Clear</a>
26
- <input
27
- ref="file"
28
- class="hidden"
29
- type="file"
30
- multiple
31
- @change="onFilesSelected"
32
- >
33
- </div>
34
23
 
35
24
  <div class="max-w-[50em] flex items-stretch justify-start">
36
25
  <FilePreview
37
26
  v-for="file in uploadedFiles"
38
27
  :key="'file-upload-' + file.id"
39
- class="w-32 m-2 cursor-pointer bg-gray-200"
28
+ class="w-32 h-32 m-2 cursor-pointer bg-gray-200"
40
29
  :class="{'border border-dashed border-blue-600': !uploadedFiles.length}"
41
- :image="file"
30
+ :file="file"
42
31
  :related-files="uploadedFiles"
43
32
  downloadable
44
33
  :removable="!readonly && !disable"
45
34
  @remove="onRemove(file)"
46
35
  />
47
- <FilePreview
36
+ <div
48
37
  v-if="!disable && !readonly"
49
- class="w-32 m-2 cursor-pointer border border-dashed border-blue-600"
50
- disabled
51
- @click="$refs.file.click()"
52
- />
38
+ class="dx-add-remove-files w-32 h-32 m-2 rounded-2xl flex flex-col flex-nowrap items-center overflow-hidden cursor-pointer"
39
+ >
40
+ <div
41
+ class="dx-add-file flex-grow p-1 pt-3 flex justify-center items-center bg-green-200 text-green-700 w-full hover:bg-green-100"
42
+ @click="$refs.file.click()"
43
+ >
44
+ <div>
45
+ <AddFileIcon class="w-10 m-auto" />
46
+ <div class="mt-1 text-center">
47
+ Add
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <div
52
+ v-if="uploadedFiles.length > 0"
53
+ class="dx-remove-file flex items-center flex-nowrap p-2 bg-red-200 text-red-800 hover:bg-red-100 w-full justify-center text-xs"
54
+ @click="clearUploadedFiles"
55
+ >
56
+ <RemoveFileIcon class="mr-2 w-3" />
57
+ Remove All
58
+ </div>
59
+ </div>
53
60
  <div
54
61
  v-if="readonly && uploadedFiles.length === 0"
55
62
  class="p-1"
@@ -60,32 +67,29 @@
60
67
  </div>
61
68
  </template>
62
69
 
63
- <script setup>
70
+ <script setup lang="ts">
64
71
  import { onMounted } from "vue";
65
72
  import { useMultiFileUpload } from "../../../../helpers";
73
+ import { ImageIcon as AddFileIcon, TrashIcon as RemoveFileIcon } from "../../../../svg";
74
+ import { FormField, UploadedFile } from "../../../../types";
66
75
  import { FilePreview } from "../../../Utility";
67
76
  import FieldLabel from "./FieldLabel";
68
77
 
69
78
  const emit = defineEmits(["update:model-value"]);
70
- const props = defineProps({
71
- modelValue: {
72
- type: [Object, String],
73
- default: null
74
- },
75
- field: {
76
- type: Object,
77
- required: true
78
- },
79
- showName: Boolean,
80
- disable: Boolean,
81
- readonly: Boolean
82
- });
79
+ const props = defineProps<{
80
+ modelValue?: UploadedFile[];
81
+ field?: FormField;
82
+ label?: string;
83
+ showName?: boolean;
84
+ disable?: boolean;
85
+ readonly?: boolean;
86
+ }>();
83
87
 
84
88
  const { onComplete, onDrop, onFilesSelected, uploadedFiles, clearUploadedFiles, onRemove } = useMultiFileUpload();
85
89
  onMounted(() => {
86
- if (props.modelValue) {
87
- uploadedFiles.value = props.modelValue;
88
- }
90
+ if (props.modelValue) {
91
+ uploadedFiles.value = props.modelValue;
92
+ }
89
93
  });
90
94
  onComplete(() => emit("update:model-value", uploadedFiles.value));
91
95
  </script>