sprintify-ui 0.0.0

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 (176) hide show
  1. package/README.md +188 -0
  2. package/dist/types/src/components/BaseAlert.vue.d.ts +51 -0
  3. package/dist/types/src/components/BaseAutocomplete.vue.d.ts +268 -0
  4. package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +273 -0
  5. package/dist/types/src/components/BaseAvatar.vue.d.ts +126 -0
  6. package/dist/types/src/components/BaseBadge.vue.d.ts +94 -0
  7. package/dist/types/src/components/BaseBelongsTo.vue.d.ts +268 -0
  8. package/dist/types/src/components/BaseBoolean.vue.d.ts +64 -0
  9. package/dist/types/src/components/BaseBreadcrumbs.vue.d.ts +66 -0
  10. package/dist/types/src/components/BaseButton.vue.d.ts +23 -0
  11. package/dist/types/src/components/BaseCard.vue.d.ts +74 -0
  12. package/dist/types/src/components/BaseCardRow.vue.d.ts +16 -0
  13. package/dist/types/src/components/BaseClipboard.vue.d.ts +74 -0
  14. package/dist/types/src/components/BaseContainer.vue.d.ts +34 -0
  15. package/dist/types/src/components/BaseCounter.vue.d.ts +125 -0
  16. package/dist/types/src/components/BaseDataIterator.vue.d.ts +345 -0
  17. package/dist/types/src/components/BaseDataTable.vue.d.ts +657 -0
  18. package/dist/types/src/components/BaseDataTableToggleColumns.vue.d.ts +1281 -0
  19. package/dist/types/src/components/BaseDatePicker.vue.d.ts +190 -0
  20. package/dist/types/src/components/BaseDateSelect.vue.d.ts +171 -0
  21. package/dist/types/src/components/BaseDescriptionList.vue.d.ts +48 -0
  22. package/dist/types/src/components/BaseDescriptionListItem.vue.d.ts +49 -0
  23. package/dist/types/src/components/BaseDialog.vue.d.ts +160 -0
  24. package/dist/types/src/components/BaseFilePicker.vue.d.ts +44 -0
  25. package/dist/types/src/components/BaseFileUploader.vue.d.ts +220 -0
  26. package/dist/types/src/components/BaseInput.vue.d.ts +209 -0
  27. package/dist/types/src/components/BaseInputLabel.vue.d.ts +31 -0
  28. package/dist/types/src/components/BaseLoadingCover.vue.d.ts +166 -0
  29. package/dist/types/src/components/BaseLoadingPage.vue.d.ts +2 -0
  30. package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +269 -0
  31. package/dist/types/src/components/BaseMediaLibraryItem.vue.d.ts +75 -0
  32. package/dist/types/src/components/BaseMenu.vue.d.ts +117 -0
  33. package/dist/types/src/components/BaseMenuItem.vue.d.ts +147 -0
  34. package/dist/types/src/components/BaseModalCenter.vue.d.ts +141 -0
  35. package/dist/types/src/components/BaseModalSide.vue.d.ts +141 -0
  36. package/dist/types/src/components/BaseNavbar.vue.d.ts +79 -0
  37. package/dist/types/src/components/BaseNavbarItem.vue.d.ts +80 -0
  38. package/dist/types/src/components/BaseNavbarItemContent.vue.d.ts +127 -0
  39. package/dist/types/src/components/BasePagination.vue.d.ts +25 -0
  40. package/dist/types/src/components/BasePaginationSimple.vue.d.ts +25 -0
  41. package/dist/types/src/components/BasePanel.vue.d.ts +31 -0
  42. package/dist/types/src/components/BasePassword.vue.d.ts +66 -0
  43. package/dist/types/src/components/BaseProcessRing.vue.d.ts +36 -0
  44. package/dist/types/src/components/BaseReadMore.vue.d.ts +74 -0
  45. package/dist/types/src/components/BaseSelect.vue.d.ts +55 -0
  46. package/dist/types/src/components/BaseSideNavigation.vue.d.ts +48 -0
  47. package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +92 -0
  48. package/dist/types/src/components/BaseSkeleton.vue.d.ts +93 -0
  49. package/dist/types/src/components/BaseSpinner.vue.d.ts +2 -0
  50. package/dist/types/src/components/BaseSwitch.vue.d.ts +39 -0
  51. package/dist/types/src/components/BaseSystemAlert.vue.d.ts +141 -0
  52. package/dist/types/src/components/BaseTabItem.vue.d.ts +70 -0
  53. package/dist/types/src/components/BaseTable.vue.d.ts +467 -0
  54. package/dist/types/src/components/BaseTableColumn.vue.d.ts +164 -0
  55. package/dist/types/src/components/BaseTabs.vue.d.ts +48 -0
  56. package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +274 -0
  57. package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +251 -0
  58. package/dist/types/src/components/BaseTextarea.vue.d.ts +228 -0
  59. package/dist/types/src/components/BaseTextareaAutoresize.vue.d.ts +44 -0
  60. package/dist/types/src/components/BaseTitle.vue.d.ts +45 -0
  61. package/dist/types/src/components/BaseWordCount.vue.d.ts +31 -0
  62. package/dist/types/src/components/SlotComponent.d.ts +43 -0
  63. package/dist/types/src/components/index.d.ts +2 -0
  64. package/dist/types/src/composables/breakpoints.d.ts +12 -0
  65. package/dist/types/src/composables/modal.d.ts +6 -0
  66. package/dist/types/src/constants/MyConstants.d.ts +1 -0
  67. package/dist/types/src/constants/index.d.ts +2 -0
  68. package/dist/types/src/index.d.ts +253 -0
  69. package/dist/types/src/types/Media.d.ts +8 -0
  70. package/dist/types/src/types/UploadedFile.d.ts +9 -0
  71. package/dist/types/src/types/User.d.ts +6 -0
  72. package/dist/types/src/types/types.d.ts +88 -0
  73. package/dist/types/src/utils/fileSizeFormat.d.ts +1 -0
  74. package/dist/types/src/utils/index.d.ts +4 -0
  75. package/dist/types/src/utils/scrollPreventer.d.ts +4 -0
  76. package/dist/types/src/utils/toHumanList.d.ts +1 -0
  77. package/package.json +99 -0
  78. package/src/assets/button.css +80 -0
  79. package/src/assets/form.css +15 -0
  80. package/src/assets/main.css +3 -0
  81. package/src/assets/pikaday.css +134 -0
  82. package/src/assets/tailwind.css +5 -0
  83. package/src/components/BaseAlert.stories.js +52 -0
  84. package/src/components/BaseAlert.vue +152 -0
  85. package/src/components/BaseAutocomplete.stories.js +127 -0
  86. package/src/components/BaseAutocomplete.vue +376 -0
  87. package/src/components/BaseAutocompleteFetch.stories.js +121 -0
  88. package/src/components/BaseAutocompleteFetch.vue +185 -0
  89. package/src/components/BaseAvatar.stories.js +39 -0
  90. package/src/components/BaseAvatar.vue +92 -0
  91. package/src/components/BaseBadge.stories.js +61 -0
  92. package/src/components/BaseBadge.vue +70 -0
  93. package/src/components/BaseBelongsTo.stories.js +130 -0
  94. package/src/components/BaseBelongsTo.vue +122 -0
  95. package/src/components/BaseBoolean.stories.js +35 -0
  96. package/src/components/BaseBoolean.vue +29 -0
  97. package/src/components/BaseBreadcrumbs.stories.js +45 -0
  98. package/src/components/BaseBreadcrumbs.vue +78 -0
  99. package/src/components/BaseButton.stories.js +80 -0
  100. package/src/components/BaseButton.vue +39 -0
  101. package/src/components/BaseCard.stories.js +61 -0
  102. package/src/components/BaseCard.vue +49 -0
  103. package/src/components/BaseCardRow.vue +34 -0
  104. package/src/components/BaseClipboard.stories.js +31 -0
  105. package/src/components/BaseClipboard.vue +96 -0
  106. package/src/components/BaseContainer.stories.js +34 -0
  107. package/src/components/BaseContainer.vue +50 -0
  108. package/src/components/BaseCounter.stories.js +32 -0
  109. package/src/components/BaseCounter.vue +72 -0
  110. package/src/components/BaseDataIterator.stories.js +90 -0
  111. package/src/components/BaseDataIterator.vue +658 -0
  112. package/src/components/BaseDataTable.stories.js +95 -0
  113. package/src/components/BaseDataTable.vue +489 -0
  114. package/src/components/BaseDataTableToggleColumns.vue +69 -0
  115. package/src/components/BaseDatePicker.stories.js +53 -0
  116. package/src/components/BaseDatePicker.vue +166 -0
  117. package/src/components/BaseDateSelect.vue +192 -0
  118. package/src/components/BaseDescriptionList.vue +11 -0
  119. package/src/components/BaseDescriptionListItem.vue +12 -0
  120. package/src/components/BaseDialog.vue +104 -0
  121. package/src/components/BaseFilePicker.vue +101 -0
  122. package/src/components/BaseFileUploader.vue +166 -0
  123. package/src/components/BaseInput.vue +82 -0
  124. package/src/components/BaseInputLabel.vue +26 -0
  125. package/src/components/BaseLoadingCover.vue +84 -0
  126. package/src/components/BaseLoadingPage.vue +19 -0
  127. package/src/components/BaseMediaLibrary.vue +281 -0
  128. package/src/components/BaseMediaLibraryItem.vue +92 -0
  129. package/src/components/BaseMenu.vue +114 -0
  130. package/src/components/BaseMenuItem.vue +93 -0
  131. package/src/components/BaseModalCenter.vue +107 -0
  132. package/src/components/BaseModalSide.vue +112 -0
  133. package/src/components/BaseNavbar.vue +72 -0
  134. package/src/components/BaseNavbarItem.vue +72 -0
  135. package/src/components/BaseNavbarItemContent.vue +57 -0
  136. package/src/components/BasePagination.vue +82 -0
  137. package/src/components/BasePaginationSimple.vue +60 -0
  138. package/src/components/BasePanel.vue +39 -0
  139. package/src/components/BasePassword.vue +73 -0
  140. package/src/components/BaseProcessRing.vue +56 -0
  141. package/src/components/BaseReadMore.vue +72 -0
  142. package/src/components/BaseSelect.vue +59 -0
  143. package/src/components/BaseSideNavigation.vue +7 -0
  144. package/src/components/BaseSideNavigationItem.vue +42 -0
  145. package/src/components/BaseSkeleton.vue +24 -0
  146. package/src/components/BaseSpinner.vue +47 -0
  147. package/src/components/BaseSwitch.vue +87 -0
  148. package/src/components/BaseSystemAlert.vue +86 -0
  149. package/src/components/BaseTabItem.vue +30 -0
  150. package/src/components/BaseTable.vue +781 -0
  151. package/src/components/BaseTableColumn.vue +109 -0
  152. package/src/components/BaseTabs.vue +12 -0
  153. package/src/components/BaseTagAutocomplete.vue +385 -0
  154. package/src/components/BaseTagAutocompleteFetch.vue +154 -0
  155. package/src/components/BaseTextarea.vue +73 -0
  156. package/src/components/BaseTextareaAutoresize.vue +117 -0
  157. package/src/components/BaseTitle.vue +80 -0
  158. package/src/components/BaseWordCount.vue +36 -0
  159. package/src/components/SlotComponent.ts +37 -0
  160. package/src/components/index.ts +5 -0
  161. package/src/composables/breakpoints.ts +6 -0
  162. package/src/composables/modal.ts +77 -0
  163. package/src/constants/MyConstants.ts +1 -0
  164. package/src/constants/index.ts +5 -0
  165. package/src/env.d.ts +15 -0
  166. package/src/index.ts +70 -0
  167. package/src/lang/en.json +56 -0
  168. package/src/lang/fr.json +56 -0
  169. package/src/types/Media.ts +9 -0
  170. package/src/types/UploadedFile.ts +10 -0
  171. package/src/types/User.ts +7 -0
  172. package/src/types/types.ts +112 -0
  173. package/src/utils/fileSizeFormat.ts +15 -0
  174. package/src/utils/index.ts +5 -0
  175. package/src/utils/scrollPreventer.ts +21 -0
  176. package/src/utils/toHumanList.ts +20 -0
@@ -0,0 +1,166 @@
1
+ <template>
2
+ <div class="relative">
3
+ <BaseFilePicker
4
+ :button-class="buttonClass"
5
+ :disabled="uploading || disabled"
6
+ :accept="accept"
7
+ @upload="onPictureUpload"
8
+ >
9
+ <template #default="slotProps">
10
+ <slot
11
+ name="default"
12
+ :uploading="uploading"
13
+ :selecting="slotProps.selecting"
14
+ :dragging="slotProps.dragging"
15
+ :disabled="slotProps.disabled"
16
+ />
17
+ <slot
18
+ name="loading"
19
+ :uploading="uploading"
20
+ :selecting="slotProps.selecting"
21
+ :dragging="slotProps.dragging"
22
+ :disabled="slotProps.disabled"
23
+ >
24
+ <BaseLoadingCover
25
+ icon-class="text-primary-600 w-6 h-6"
26
+ :model-value="loading || uploading || slotProps.selecting"
27
+ />
28
+ </slot>
29
+ </template>
30
+ </BaseFilePicker>
31
+ </div>
32
+ </template>
33
+
34
+ <script lang="ts" setup>
35
+ import { config } from 'src';
36
+ import { PropType } from 'vue';
37
+ import { UploadedFile } from '@/types/UploadedFile';
38
+ import { toHumanList, fileSizeFormat } from '../utils';
39
+ import { useNotificationsStore } from '../stores/notifications';
40
+
41
+ const http = config.http;
42
+ const i18n = useI18n();
43
+ const notifications = useNotificationsStore();
44
+
45
+ const props = defineProps({
46
+ disabled: {
47
+ default: false,
48
+ type: Boolean,
49
+ },
50
+ loading: {
51
+ default: false,
52
+ type: Boolean,
53
+ },
54
+ beforeUpload: {
55
+ default: (): boolean => {
56
+ return true;
57
+ },
58
+ type: Function as PropType<() => boolean>,
59
+ },
60
+ buttonClass: {
61
+ default: '',
62
+ type: String,
63
+ },
64
+ maxSize: {
65
+ default: 1024 * 1024 * 20, // 20 MB,
66
+ type: Number,
67
+ },
68
+ accept: {
69
+ default: undefined,
70
+ type: String,
71
+ },
72
+ acceptedExtensions: {
73
+ default: undefined,
74
+ type: Array as PropType<string[]>,
75
+ },
76
+ });
77
+
78
+ const emit = defineEmits([
79
+ 'upload:start',
80
+ 'upload:success',
81
+ 'upload:fail',
82
+ 'upload:end',
83
+ ]);
84
+
85
+ const uploading = ref(false);
86
+
87
+ async function onPictureUpload(file: File) {
88
+ if (!(await props.beforeUpload())) {
89
+ return;
90
+ }
91
+
92
+ try {
93
+ if (file.size > props.maxSize) {
94
+ notifications.push({
95
+ color: 'danger',
96
+ title: i18n.t('sui.error'),
97
+ text: i18n.t('sui.the_file_size_must_not_exceed_x', {
98
+ x: fileSizeFormat(props.maxSize),
99
+ }),
100
+ });
101
+ return;
102
+ }
103
+
104
+ const extension = file.name.split('.').pop();
105
+
106
+ if (
107
+ extension &&
108
+ props.acceptedExtensions &&
109
+ props.acceptedExtensions.length
110
+ ) {
111
+ if (!props.acceptedExtensions.includes(extension)) {
112
+ notifications.push({
113
+ color: 'danger',
114
+ title: i18n.t('sui.error'),
115
+ text:
116
+ i18n.t('sui.the_file_type_is_invalid') +
117
+ ' ' +
118
+ i18n.t('sui.file_must_be_of_type') +
119
+ ' ' +
120
+ toHumanList(props.acceptedExtensions, i18n.t('sui.or')) +
121
+ '.',
122
+ });
123
+ return;
124
+ }
125
+ }
126
+
127
+ const formData = new FormData();
128
+
129
+ formData.append('file', file);
130
+
131
+ uploading.value = true;
132
+ emit('upload:start');
133
+
134
+ const response = await http.post(config.upload_url, formData);
135
+
136
+ const payload = response.data as UploadedFile;
137
+ payload.original_file = file;
138
+
139
+ const reader = new FileReader();
140
+
141
+ reader.onload = (e: any) => {
142
+ payload.data_url = e.target.result;
143
+ onSuccess(payload);
144
+ };
145
+
146
+ reader.onerror = (e: any) => {
147
+ onSuccess(payload);
148
+ };
149
+
150
+ if (payload.mime_type.includes('image')) {
151
+ reader.readAsDataURL(file);
152
+ } else {
153
+ onSuccess(payload);
154
+ }
155
+ } catch (e: unknown) {
156
+ emit('upload:fail');
157
+ } finally {
158
+ emit('upload:end');
159
+ uploading.value = false;
160
+ }
161
+ }
162
+
163
+ function onSuccess(payload: any) {
164
+ emit('upload:success', payload);
165
+ }
166
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <input
3
+ ref="input"
4
+ :value="modelValue"
5
+ :type="type"
6
+ :name="name"
7
+ :disabled="disabled"
8
+ :placeholder="placeholder"
9
+ :required="required"
10
+ class="rounded"
11
+ :autocomplete="autocomplete ? 'on' : 'off'"
12
+ @keydown.enter="onEnter"
13
+ @input="$emit('update:modelValue', transformInputValue($event))"
14
+ />
15
+ </template>
16
+
17
+ <script lang="ts" setup>
18
+ import { get, isNumber, isString, trim } from 'lodash';
19
+ import { PropType } from 'vue';
20
+
21
+ const props = defineProps({
22
+ modelValue: {
23
+ required: true,
24
+ type: [String, Number, null] as PropType<string | number | null>,
25
+ },
26
+ type: {
27
+ type: String,
28
+ default: 'text',
29
+ },
30
+ autocomplete: {
31
+ default: true,
32
+ type: Boolean,
33
+ },
34
+ preventSubmit: {
35
+ default: false,
36
+ type: Boolean,
37
+ },
38
+ name: {
39
+ default: undefined,
40
+ type: String,
41
+ },
42
+ placeholder: {
43
+ default: '',
44
+ type: String,
45
+ },
46
+ disabled: {
47
+ default: false,
48
+ type: Boolean,
49
+ },
50
+ required: {
51
+ default: false,
52
+ type: Boolean,
53
+ },
54
+ });
55
+
56
+ defineEmits(['update:modelValue']);
57
+
58
+ function transformInputValue(event: Event | null): string | null {
59
+ if (event === null) {
60
+ return null;
61
+ }
62
+
63
+ const value = get(event, 'target.value', null);
64
+
65
+ if (isString(value)) {
66
+ return trim(value);
67
+ }
68
+
69
+ if (isNumber(value)) {
70
+ return trim(value + '');
71
+ }
72
+
73
+ return '';
74
+ }
75
+
76
+ function onEnter(e: Event) {
77
+ if (props.preventSubmit) {
78
+ e.preventDefault();
79
+ return;
80
+ }
81
+ }
82
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <label :class="classes"
3
+ >{{ label }}<span v-if="required" class="text-red-600"> *</span></label
4
+ >
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ import { defineComponent } from 'vue';
9
+
10
+ export default defineComponent({
11
+ props: {
12
+ required: {
13
+ default: false,
14
+ type: Boolean,
15
+ },
16
+ label: {
17
+ required: true,
18
+ type: String,
19
+ },
20
+ classes: {
21
+ default: 'form-input-label mb-1',
22
+ type: String,
23
+ },
24
+ },
25
+ });
26
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <Transition
3
+ :enter-active-class="'transition ease-out ' + duration"
4
+ enter-from-class="opacity-0"
5
+ enter-to-class="opacity-100"
6
+ :leave-active-class="'transition ease-in ' + duration"
7
+ leave-from-class="opacity-100"
8
+ leave-to-class="opacity-0"
9
+ >
10
+ <div
11
+ v-if="showSpinner"
12
+ class="absolute inset-0 flex h-full w-full items-center justify-center"
13
+ >
14
+ <div class="absolute h-full w-full" :class="backdropClass" />
15
+ <svg
16
+ v-if="icon == 'line'"
17
+ class="animate-spin"
18
+ :class="iconClass"
19
+ viewBox="0 0 24 24"
20
+ >
21
+ <path
22
+ fill="currentColor"
23
+ d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"
24
+ />
25
+ </svg>
26
+ <BaseSpinner v-else-if="icon == 'spinner'" :class="iconClass" />
27
+ </div>
28
+ </Transition>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import { PropType } from 'vue';
33
+ import BaseSpinner from './BaseSpinner.vue';
34
+
35
+ const props = defineProps({
36
+ modelValue: {
37
+ default: true,
38
+ type: Boolean,
39
+ },
40
+ backdropClass: {
41
+ default: 'bg-white',
42
+ type: String,
43
+ },
44
+ iconClass: {
45
+ default: 'text-blue-500 w-10 h-10',
46
+ type: String,
47
+ },
48
+ icon: {
49
+ default: 'line',
50
+ type: String as PropType<'line' | 'spinner'>,
51
+ },
52
+ duration: {
53
+ default: 'duration-300',
54
+ type: String,
55
+ },
56
+ delay: {
57
+ default: 330,
58
+ type: Number,
59
+ },
60
+ });
61
+
62
+ let timeoutId = null as number | null;
63
+
64
+ const showSpinner = ref(false);
65
+
66
+ watch(
67
+ () => props.modelValue,
68
+ (newValue: boolean) => {
69
+ if (newValue) {
70
+ timeoutId = setTimeout(() => {
71
+ showSpinner.value = true;
72
+ }, props.delay);
73
+ } else {
74
+ if (timeoutId) {
75
+ clearTimeout(timeoutId);
76
+ }
77
+ showSpinner.value = false;
78
+ }
79
+ },
80
+ {
81
+ immediate: true,
82
+ }
83
+ );
84
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div
3
+ class="fixed inset-0 z-loading flex h-full w-full items-center justify-center bg-white"
4
+ >
5
+ <div class="flex flex-col items-center">
6
+ <img
7
+ :src="'/img/logo/icon-color.svg'"
8
+ alt="Loading"
9
+ class="mb-14 w-16 animate-pulse"
10
+ />
11
+ <svg class="h-6 w-6 animate-spin text-primary-500" viewBox="0 0 24 24">
12
+ <path
13
+ fill="currentColor"
14
+ d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"
15
+ />
16
+ </svg>
17
+ </div>
18
+ </div>
19
+ </template>
@@ -0,0 +1,281 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ v-if="currentMediaInternal.length + normalizedModelValue.to_add.length"
5
+ class="mb-5"
6
+ >
7
+ <div class="flex flex-wrap">
8
+ <BaseMediaLibraryItem
9
+ v-for="(media, index) in currentMediaInternal"
10
+ :key="media.id"
11
+ :media="media"
12
+ @delete="promptRemoveMedia(index)"
13
+ >
14
+ {{ media.file_name }}
15
+ </BaseMediaLibraryItem>
16
+ <BaseMediaLibraryItem
17
+ v-for="(file, index) in normalizedModelValue.to_add"
18
+ :key="file.id"
19
+ :media="file"
20
+ @delete="promptRemoveUploadedFile(index)"
21
+ >
22
+ {{ file.file_name }}
23
+ </BaseMediaLibraryItem>
24
+ </div>
25
+ </div>
26
+
27
+ <BaseFileUploader
28
+ :max-size="maxSize"
29
+ :disabled="disabled"
30
+ class="w-full"
31
+ button-class="w-full"
32
+ :accept="accept"
33
+ :accepted-extensions="acceptedExtensions"
34
+ @upload:start="$emit('upload:start', $event)"
35
+ @upload:end="$emit('upload:end', $event)"
36
+ @upload:fail="$emit('upload:fail', $event)"
37
+ @upload:success="onUploadSuccess"
38
+ >
39
+ <template #default="baseFileUploaderProps">
40
+ <slot
41
+ name="default"
42
+ v-bind="baseFileUploaderProps"
43
+ :max-size="maxSize"
44
+ :max="max"
45
+ >
46
+ <div
47
+ class="flex w-full items-start space-x-4 rounded-lg border-2 border-dashed border-slate-200 p-5 duration-200 hover:bg-slate-50 hover:enabled:bg-slate-100"
48
+ :class="[
49
+ baseFileUploaderProps.dragging ? 'bg-slate-100' : 'bg-white',
50
+ baseFileUploaderProps.disabled ? 'opacity-25' : '',
51
+ ]"
52
+ >
53
+ <div class="rounded-full bg-slate-200 p-2">
54
+ <Icon
55
+ icon="heroicons:arrow-up-on-square"
56
+ class="h-6 w-6 text-slate-500"
57
+ />
58
+ </div>
59
+ <div class="text-left">
60
+ <p class="mb-0 text-sm font-medium leading-tight">
61
+ {{ $t('sui.drop_or_click_to_upload') }}
62
+ </p>
63
+
64
+ <div class="mt-1 text-sm leading-tight text-slate-500">
65
+ <p v-if="max > 1">
66
+ {{ $t('sui.you_can_upload_up_to_n_files', { count: max }) }}
67
+ </p>
68
+ <p>
69
+ {{
70
+ capitalize(
71
+ $t('sui.up_to_x', { x: fileSizeFormat(maxSize) })
72
+ )
73
+ }}
74
+ </p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </slot>
79
+ </template>
80
+ </BaseFileUploader>
81
+
82
+ <p class="text-red-600">
83
+ {{ globalErrorMessage }}
84
+ </p>
85
+ </div>
86
+ </template>
87
+
88
+ <script lang="ts" setup>
89
+ import { UploadedFile } from '@/types/UploadedFile';
90
+ import { Media } from '@/types/Media';
91
+ import _, { cloneDeep, get } from 'lodash';
92
+ import { PropType } from 'vue';
93
+ import { MediaLibraryPayload } from '@/types/types';
94
+ import { useDialogsStore } from '../stores/dialogs';
95
+ import { useNotificationsStore } from '../stores/notifications';
96
+ import { capitalize } from 'lodash';
97
+ import { fileSizeFormat } from 'src/utils';
98
+
99
+ const i18n = useI18n();
100
+ const dialogs = useDialogsStore();
101
+ const notifications = useNotificationsStore();
102
+
103
+ const emit = defineEmits([
104
+ 'update',
105
+ 'upload:start',
106
+ 'upload:success',
107
+ 'upload:fail',
108
+ 'upload:end',
109
+ ]);
110
+
111
+ const props = defineProps({
112
+ modelValue: {
113
+ default: undefined,
114
+ type: Object as PropType<MediaLibraryPayload | null | undefined>,
115
+ },
116
+ name: {
117
+ required: true,
118
+ type: String,
119
+ },
120
+ min: {
121
+ type: Number,
122
+ default: undefined,
123
+ },
124
+ max: {
125
+ default: 100,
126
+ type: Number,
127
+ },
128
+ maxSize: {
129
+ default: 20 * 1024 * 1024,
130
+ type: Number,
131
+ },
132
+ accept: {
133
+ default: undefined,
134
+ type: String,
135
+ },
136
+ acceptedExtensions: {
137
+ default: undefined,
138
+ type: Array as PropType<string[]>,
139
+ },
140
+ currentMedia: {
141
+ default() {
142
+ return [];
143
+ },
144
+ type: Array as PropType<Media[]>,
145
+ },
146
+ errors: {
147
+ default: undefined,
148
+ type: Object as PropType<Record<string, string[]>>,
149
+ },
150
+ disabled: {
151
+ default: false,
152
+ type: Boolean,
153
+ },
154
+ });
155
+
156
+ const currentMediaInternal = ref(cloneDeep(props.currentMedia));
157
+
158
+ const normalizedModelValue = computed(() => {
159
+ if (
160
+ props.modelValue &&
161
+ _.isObject(props.modelValue) &&
162
+ _.isArray(props.modelValue.to_add) &&
163
+ _.isArray(props.modelValue.to_remove)
164
+ ) {
165
+ return props.modelValue;
166
+ }
167
+
168
+ return {
169
+ to_add: [],
170
+ to_remove: [],
171
+ };
172
+ });
173
+
174
+ const numberOfFiles = computed((): number => {
175
+ return (
176
+ currentMediaInternal.value.length +
177
+ (props.modelValue?.to_add.length ?? 0) -
178
+ (props.modelValue?.to_remove.length ?? 0)
179
+ );
180
+ });
181
+
182
+ sync(normalizedModelValue.value);
183
+
184
+ function onUploadSuccess(file: UploadedFile) {
185
+ if (file == null) {
186
+ return;
187
+ }
188
+
189
+ if (numberOfFiles.value >= props.max && props.max > 1) {
190
+ notifications.push({
191
+ title: i18n.t('sui.whoops'),
192
+ text: i18n.t('sui.you_can_upload_up_to_n_files', { count: props.max }),
193
+ color: 'danger',
194
+ });
195
+ return;
196
+ }
197
+
198
+ const modelValue = cloneDeep(normalizedModelValue.value);
199
+
200
+ if (props.max == 1) {
201
+ // Remove everything...
202
+ modelValue.to_remove.push(...currentMediaInternal.value.map((m) => m.id));
203
+ modelValue.to_add = [];
204
+ currentMediaInternal.value = [];
205
+ }
206
+
207
+ modelValue.to_add.push(file);
208
+
209
+ sync(modelValue);
210
+
211
+ emit('upload:success', file);
212
+ }
213
+
214
+ function promptRemoveUploadedFile(index: number, length = 1) {
215
+ dialogs.push({
216
+ title: i18n.t('sui.remove_file'),
217
+ message: i18n.t('sui.remove_file_description'),
218
+ color: 'warning',
219
+ onConfirm() {
220
+ removeUploadedFile(index, length);
221
+ },
222
+ });
223
+ }
224
+
225
+ function promptRemoveMedia(index: number) {
226
+ dialogs.push({
227
+ title: i18n.t('sui.remove_file'),
228
+ message: i18n.t('sui.remove_file_description'),
229
+ color: 'warning',
230
+ onConfirm() {
231
+ removeMedia(index);
232
+ },
233
+ });
234
+ }
235
+
236
+ function removeUploadedFile(index: number, length = 1) {
237
+ const modelValue = cloneDeep(normalizedModelValue.value);
238
+
239
+ modelValue?.to_add.splice(index, length);
240
+
241
+ sync(modelValue);
242
+ }
243
+
244
+ function removeMedia(index: number) {
245
+ const media = currentMediaInternal.value[index];
246
+
247
+ if (media) {
248
+ const modelValue = cloneDeep(normalizedModelValue.value);
249
+
250
+ modelValue.to_remove.push(media.id);
251
+
252
+ sync(modelValue);
253
+
254
+ currentMediaInternal.value.splice(index, 1);
255
+ }
256
+ }
257
+
258
+ function sync(modelValue: MediaLibraryPayload) {
259
+ emit('update', modelValue);
260
+ }
261
+
262
+ function errorMessage(name: string): string {
263
+ const errors = get(props.errors, name, []);
264
+
265
+ if (errors.length == 0) {
266
+ return '';
267
+ }
268
+
269
+ return errors[0];
270
+ }
271
+
272
+ const globalErrorMessage = computed(() => {
273
+ if (errorMessage(props.name + '.to_add.0')) {
274
+ return errorMessage(props.name + '.to_add.0');
275
+ }
276
+ if (errorMessage(props.name + '.to_remove')) {
277
+ return errorMessage(props.name + '.to_remove');
278
+ }
279
+ return '';
280
+ });
281
+ </script>