@dejwcake/craftable 0.1.10 → 2.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 (126) hide show
  1. package/README.md +117 -3
  2. package/dist/buildMatchPatternFn-BV4I8r8j.js +63 -0
  3. package/dist/buildMatchPatternFn-T5KjFTBQ.cjs +1 -0
  4. package/dist/craftable.css +2 -0
  5. package/dist/cs-B5QMxFrl.js +682 -0
  6. package/dist/cs-MAoetkhe.cjs +1 -0
  7. package/dist/de-BlQ2ZKmc.js +526 -0
  8. package/dist/de-BoVLAn6U.cjs +1 -0
  9. package/dist/en-US-BLvEnhs4.js +443 -0
  10. package/dist/en-US-Dz8vkDZ_.cjs +1 -0
  11. package/dist/en-US-X9gX759i.cjs +1 -0
  12. package/dist/es-DBHDeXHI.cjs +1 -0
  13. package/dist/es-YEqfkBY2.js +437 -0
  14. package/dist/fr-B7MHC2AN.js +407 -0
  15. package/dist/fr-DLs3QoNB.cjs +1 -0
  16. package/dist/hu-BKZPI9jH.cjs +1 -0
  17. package/dist/hu-reoJgNxD.js +425 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.mjs +3866 -0
  20. package/dist/isSameWeek-CC6GX_zp.js +40 -0
  21. package/dist/isSameWeek-Mctj6Tdj.cjs +1 -0
  22. package/dist/pl-B5A0Rjp7.js +628 -0
  23. package/dist/pl-BMyrOO-Q.cjs +1 -0
  24. package/dist/sk-B7JF2MoV.cjs +1 -0
  25. package/dist/sk-Btro3W4S.js +602 -0
  26. package/package.json +68 -40
  27. package/scripts/publish-components.js +118 -0
  28. package/scss/_images.scss +1 -1
  29. package/scss/_variables.scss +13 -6
  30. package/scss/app.scss +4 -4
  31. package/scss/overrides/_alerts.scss +0 -2
  32. package/scss/overrides/_badges.scss +1 -17
  33. package/scss/overrides/_buttons.scss +64 -7
  34. package/scss/overrides/_cards.scss +4 -51
  35. package/scss/overrides/_checkboxes.scss +54 -59
  36. package/scss/overrides/_forms.scss +270 -176
  37. package/scss/overrides/_index.scss +18 -18
  38. package/scss/overrides/_keyframes.scss +6 -4
  39. package/scss/overrides/_layout.scss +24 -30
  40. package/scss/overrides/_media-wysiwyg.scss +9 -111
  41. package/scss/overrides/_modal.scss +0 -18
  42. package/scss/overrides/_multiselect.scss +63 -7
  43. package/scss/overrides/_nav.scss +55 -33
  44. package/scss/overrides/_notifications.scss +6 -1
  45. package/scss/overrides/_pagination.scss +3 -3
  46. package/scss/overrides/_sidebar.scss +84 -0
  47. package/scss/overrides/_tables.scss +52 -37
  48. package/scss/overrides/_text.scss +1 -3
  49. package/scss/overrides/_wysiwyg.scss +151 -48
  50. package/scss/styles/_auth.scss +32 -37
  51. package/scss/styles/_custom.scss +44 -1
  52. package/scss/styles/_index.scss +4 -4
  53. package/scss/styles/_legacy.scss +5 -81
  54. package/scss/styles/_others.scss +232 -337
  55. package/scss/vendor/_index.scss +5 -4
  56. package/src/auth/ActivationError.vue +23 -0
  57. package/src/auth/ActivationForm.vue +69 -0
  58. package/src/auth/ForgotPasswordForm.vue +74 -0
  59. package/src/auth/LoginForm.vue +107 -0
  60. package/src/auth/ResetPasswordForm.vue +122 -0
  61. package/src/components/ConfirmModal.vue +65 -0
  62. package/src/components/UserDetailTooltip.vue +88 -0
  63. package/src/components/form/CkeditorEditor.vue +205 -0
  64. package/src/components/form/FormCheckbox.vue +29 -0
  65. package/src/components/form/FormDatePicker.vue +52 -0
  66. package/src/components/form/FormEmail.vue +17 -0
  67. package/src/components/form/FormInput.vue +31 -0
  68. package/src/components/form/FormLocalizedInput.vue +47 -0
  69. package/src/components/form/FormLocalizedWysiwyg.vue +55 -0
  70. package/src/components/form/FormLocalizedWysiwygSimple.vue +55 -0
  71. package/src/components/form/FormMultiSelect.vue +36 -0
  72. package/src/components/form/FormPasswordConfirm.vue +49 -0
  73. package/src/components/form/FormSelect.vue +35 -0
  74. package/src/components/form/FormSubmit.vue +13 -0
  75. package/src/components/form/FormTextarea.vue +31 -0
  76. package/src/components/form/FormWysiwyg.vue +31 -0
  77. package/src/components/form/FormWysiwygSimple.vue +31 -0
  78. package/src/components/form/LocalizationBar.vue +90 -0
  79. package/src/components/form/MediaUpload.vue +317 -0
  80. package/src/components/form/TiptapEditor.vue +238 -0
  81. package/src/components/listing/BulkCheckboxHeader.vue +18 -0
  82. package/src/components/listing/BulkCheckboxRow.vue +21 -0
  83. package/src/components/listing/BulkOperationsBar.vue +41 -0
  84. package/src/components/listing/DeleteButton.vue +46 -0
  85. package/src/components/listing/EditButton.vue +17 -0
  86. package/src/components/listing/EmptyState.vue +25 -0
  87. package/src/components/listing/ListingHeader.vue +24 -0
  88. package/src/components/listing/Pagination.vue +158 -0
  89. package/src/components/listing/PerPage.vue +15 -0
  90. package/src/components/listing/PublishedAtColumn.vue +226 -0
  91. package/src/components/listing/Search.vue +81 -0
  92. package/src/components/listing/Sortable.vue +28 -0
  93. package/src/components/listing/ToggleSwitch.vue +47 -0
  94. package/src/composables/useAdmin.js +39 -0
  95. package/src/composables/useBaseAuth.js +81 -0
  96. package/src/composables/useBaseForm.js +265 -0
  97. package/src/composables/useBaseListing.js +316 -0
  98. package/src/composables/useResponsiveColumns.js +62 -0
  99. package/src/index.js +62 -0
  100. package/src/translation/TranslationListing.vue +230 -0
  101. package/src/translation/modals/TranslationEditModal.vue +86 -0
  102. package/src/translation/modals/TranslationExportModal.vue +74 -0
  103. package/src/translation/modals/TranslationImportModal.vue +255 -0
  104. package/src/ui/index.js +99 -0
  105. package/src/utils/ckeditorHelpers.js +36 -0
  106. package/src/utils/dateFnsLocale.js +24 -0
  107. package/src/utils/dateFormatters.js +30 -0
  108. package/src/utils/mediaProps.js +14 -0
  109. package/src/utils/notify.js +22 -0
  110. package/dist/admin/Admin.js +0 -42
  111. package/dist/auth/Form.js +0 -15
  112. package/dist/base-components/Form/BaseForm.js +0 -395
  113. package/dist/base-components/Form/BaseUpload.js +0 -188
  114. package/dist/base-components/Form/bootstrap.js +0 -21
  115. package/dist/base-components/Listing/BaseListing.js +0 -454
  116. package/dist/base-components/Listing/components/Pagination.js +0 -105
  117. package/dist/base-components/Listing/components/Sortable.js +0 -48
  118. package/dist/base-components/Listing/components/UserDetailTooltip.js +0 -55
  119. package/dist/index.js +0 -70
  120. package/dist/overrides/trumbowyg.edit-embed-template.js +0 -72
  121. package/dist/overrides/trumbowyg.reupload.js +0 -146
  122. package/dist/overrides/trumbowyg.template.js +0 -67
  123. package/dist/translation/Form.js +0 -15
  124. package/dist/translation/Listing.js +0 -283
  125. package/dist/ui/index.js +0 -33
  126. package/scss/overrides/_switches.scss +0 -25
@@ -0,0 +1,230 @@
1
+ <template>
2
+ <div class="row">
3
+ <div class="col">
4
+ <TranslationEditModal
5
+ :show="showEditModal"
6
+ :item="editItem"
7
+ :locales="localeList"
8
+ :url="url"
9
+ :translations="translations"
10
+ @close="showEditModal = false"
11
+ @saved="loadData"
12
+ />
13
+
14
+ <TranslationImportModal
15
+ :show="showImportModal"
16
+ :locales="localeList"
17
+ :url="url"
18
+ :step-count="stepCount"
19
+ :translations="translations"
20
+ @close="showImportModal = false"
21
+ @imported="loadData"
22
+ />
23
+
24
+ <TranslationExportModal
25
+ :show="showExportModal"
26
+ :locales="localeList"
27
+ :url="url"
28
+ :translations="translations"
29
+ @close="showExportModal = false"
30
+ />
31
+
32
+ <!-- Main card -->
33
+ <div class="card">
34
+ <div class="card-header">
35
+ <i class="fa fa-align-justify"></i> {{ translations.listing_title }}
36
+ <a
37
+ class="btn btn-primary btn-sm float-end ms-2"
38
+ href="#"
39
+ role="button"
40
+ @click.prevent="showImportModal = true"
41
+ >
42
+ <i class="fa fa-upload"></i>&nbsp; {{ translations.import_btn }}
43
+ </a>
44
+ <a
45
+ class="btn btn-primary btn-sm float-end ms-2"
46
+ href="#"
47
+ role="button"
48
+ @click.prevent="showExportModal = true"
49
+ >
50
+ <i class="fa fa-file-excel"></i>&nbsp; {{ translations.export_btn }}
51
+ </a>
52
+ <a class="btn btn-primary btn-sm float-end" href="#" role="button" @click.prevent="rescan">
53
+ <i class="fa" :class="scanning ? 'fa-spinner fa-spin' : 'fa-eye'"></i>&nbsp;
54
+ {{ translations.rescan_btn }}
55
+ </a>
56
+ </div>
57
+ <div ref="cardBody" class="card-body">
58
+ <div class="row justify-content-md-between mb-3">
59
+ <div class="col col-lg-7 col-xl-5 mb-3">
60
+ <Search
61
+ v-model:search="search"
62
+ :filter-fn="filter"
63
+ :translations="{
64
+ search_placeholder: translations.search_placeholder,
65
+ search_btn: translations.search_btn,
66
+ }"
67
+ >
68
+ <template #prepend>
69
+ <Multiselect
70
+ :model-value="filters.group"
71
+ :options="groups"
72
+ :placeholder="translations.all_groups"
73
+ :allow-empty="true"
74
+ :show-labels="false"
75
+ open-direction="bottom"
76
+ class="multiselect--input-group-prepend"
77
+ @update:model-value="onGroupSelect"
78
+ />
79
+ </template>
80
+ </Search>
81
+ </div>
82
+ <div class="col-sm-auto mb-3">
83
+ <PerPage v-model="pagination.state.per_page" />
84
+ </div>
85
+ </div>
86
+
87
+ <div class="table-responsive">
88
+ <table class="table table-hover table-listing">
89
+ <thead>
90
+ <tr>
91
+ <Sortable :column="'group'" :order-by="orderBy" @sort="onSort">{{
92
+ translations.group
93
+ }}</Sortable>
94
+ <Sortable :column="'key'" :order-by="orderBy" @sort="onSort">{{
95
+ translations.default
96
+ }}</Sortable>
97
+ <Sortable :column="'text'" :order-by="orderBy" @sort="onSort">{{
98
+ userLocale.toUpperCase()
99
+ }}</Sortable>
100
+ <Sortable
101
+ v-if="isColumnVisible(4)"
102
+ :column="'created_at'"
103
+ :order-by="orderBy"
104
+ @sort="onSort"
105
+ >{{ translations.created_at_label }}</Sortable
106
+ >
107
+ <th></th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <tr v-for="item in collection" :key="item.id">
112
+ <td>{{ item.group }}</td>
113
+ <td>{{ item.key }}</td>
114
+ <td>{{ item.text?.[userLocale] }}</td>
115
+ <td v-if="isColumnVisible(4)">{{ formatDate(item.created_at) }}</td>
116
+ <td>
117
+ <a
118
+ class="btn btn-sm btn-info"
119
+ href="#"
120
+ :title="translations.edit_btn"
121
+ role="button"
122
+ @click.prevent="openEditModal(item)"
123
+ >
124
+ <i class="fa fa-edit"></i>
125
+ </a>
126
+ </td>
127
+ </tr>
128
+ </tbody>
129
+ </table>
130
+ </div>
131
+
132
+ <Pagination
133
+ :pagination="pagination.state"
134
+ :options="pagination.options"
135
+ :translations="{
136
+ previous: translations.pagination_previous,
137
+ next: translations.pagination_next,
138
+ overview: translations.pagination_overview,
139
+ }"
140
+ @page-change="onPageChange"
141
+ @per-page-change="onPerPageChange"
142
+ />
143
+
144
+ <div v-if="!collection?.length" class="no-items-found">
145
+ <i class="fa fa-magnifying-glass"></i>
146
+ <h3>{{ translations.no_items }}</h3>
147
+ <p>{{ translations.try_changing_items }}</p>
148
+ <a class="btn btn-primary" href="#" role="button" @click.prevent="rescan">
149
+ <i class="fa" :class="scanning ? 'fa-spinner fa-spin' : 'fa-eye'"></i>&nbsp;
150
+ {{ translations.rescan_btn }}
151
+ </a>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </template>
158
+
159
+ <script setup>
160
+ import { ref, computed } from 'vue';
161
+ import { useBaseListing } from '../composables/useBaseListing.js';
162
+ import { useResponsiveColumns } from '../composables/useResponsiveColumns.js';
163
+ import { notifyError } from '../utils/notify.js';
164
+ import { formatDate } from '../utils/dateFormatters.js';
165
+ import Multiselect from 'vue-multiselect';
166
+ import Sortable from '../components/listing/Sortable.vue';
167
+ import Pagination from '../components/listing/Pagination.vue';
168
+ import PerPage from '../components/listing/PerPage.vue';
169
+ import Search from '../components/listing/Search.vue';
170
+ import TranslationEditModal from './modals/TranslationEditModal.vue';
171
+ import TranslationImportModal from './modals/TranslationImportModal.vue';
172
+ import TranslationExportModal from './modals/TranslationExportModal.vue';
173
+
174
+ const axios = window.axios;
175
+
176
+ const props = defineProps({
177
+ url: { type: String, required: true },
178
+ data: { type: Object, default: null },
179
+ timezone: { type: String, default: 'UTC' },
180
+ locales: { type: [Object, Array], default: () => [] },
181
+ userLocale: { type: String, default: 'en' },
182
+ groups: { type: Array, default: () => [] },
183
+ translations: { type: Object, default: () => ({}) },
184
+ stepCount: { type: Number, default: 3 },
185
+ });
186
+
187
+ const cardBody = ref(null);
188
+ const { isColumnVisible } = useResponsiveColumns(cardBody);
189
+
190
+ const { pagination, orderBy, filters, search, collection, loadData, filter, onSort, onPageChange, onPerPageChange } =
191
+ useBaseListing(props);
192
+
193
+ const localeList = computed(() => (Array.isArray(props.locales) ? props.locales : Object.values(props.locales)));
194
+
195
+ // Modal state
196
+ const showEditModal = ref(false);
197
+ const showImportModal = ref(false);
198
+ const showExportModal = ref(false);
199
+ const editItem = ref(null);
200
+
201
+ // Group filter
202
+ filters.group = null;
203
+
204
+ function onGroupSelect(val) {
205
+ filters.group = val ?? null;
206
+ loadData(true);
207
+ }
208
+
209
+ function openEditModal(item) {
210
+ editItem.value = item;
211
+ showEditModal.value = true;
212
+ }
213
+
214
+ // Rescan
215
+ const scanning = ref(false);
216
+
217
+ function rescan() {
218
+ scanning.value = true;
219
+ axios.post(props.url + '/rescan').then(
220
+ () => {
221
+ scanning.value = false;
222
+ loadData(true);
223
+ },
224
+ () => {
225
+ scanning.value = false;
226
+ notifyError();
227
+ },
228
+ );
229
+ }
230
+ </script>
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="modal fade" :class="{ show: show }" :style="{ display: show ? 'block' : 'none' }">
3
+ <div class="modal-dialog">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h4 class="modal-title">{{ translations.edit }}</h4>
7
+ <button type="button" class="btn-close" @click="$emit('close')"></button>
8
+ </div>
9
+ <div class="modal-body">
10
+ <p class="text-center" style="word-wrap: break-word">
11
+ <strong>{{ translations.default_text }}:</strong> {{ item?.key }}
12
+ </p>
13
+ <form @submit.prevent="onSubmit">
14
+ <div v-for="locale in localeList" :key="locale" class="mb-3">
15
+ <label class="form-label">{{ locale.toUpperCase() }} {{ translations.translation }}</label>
16
+ <input
17
+ v-if="!translationValues[locale] || translationValues[locale].length < 70"
18
+ v-model="translationValues[locale]"
19
+ type="text"
20
+ class="form-control"
21
+ :placeholder="(translations.translation_for || '').replace(':locale', locale)"
22
+ />
23
+ <textarea
24
+ v-else
25
+ v-model="translationValues[locale]"
26
+ class="form-control"
27
+ :placeholder="(translations.translation_for || '').replace(':locale', locale)"
28
+ cols="30"
29
+ rows="4"
30
+ ></textarea>
31
+ </div>
32
+ <div class="text-center">
33
+ <button class="btn btn-primary w-100" type="submit">
34
+ {{ translations.save }} {{ translations.translation }}
35
+ </button>
36
+ </div>
37
+ </form>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div v-if="show" class="modal-backdrop fade show" @click="$emit('close')"></div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { reactive, computed, watch } from 'vue';
47
+ import { notifySuccess, notifyError } from '../../utils/notify.js';
48
+
49
+ const axios = window.axios;
50
+
51
+ const props = defineProps({
52
+ show: { type: Boolean, default: false },
53
+ item: { type: Object, default: null },
54
+ locales: { type: [Object, Array], default: () => [] },
55
+ url: { type: String, required: true },
56
+ translations: { type: Object, default: () => ({}) },
57
+ });
58
+
59
+ const emit = defineEmits(['close', 'saved']);
60
+
61
+ const localeList = computed(() => (Array.isArray(props.locales) ? props.locales : Object.values(props.locales)));
62
+
63
+ const translationValues = reactive({});
64
+
65
+ watch(
66
+ () => props.item,
67
+ (item) => {
68
+ Object.keys(translationValues).forEach((key) => delete translationValues[key]);
69
+ for (const key of Object.keys(item?.text || {})) {
70
+ translationValues[key] = item.text[key];
71
+ }
72
+ },
73
+ { immediate: true },
74
+ );
75
+
76
+ async function onSubmit() {
77
+ try {
78
+ await axios.post(props.url + '/' + props.item.id, { text: { ...translationValues } });
79
+ notifySuccess('Item successfully changed.');
80
+ emit('saved');
81
+ emit('close');
82
+ } catch {
83
+ notifyError();
84
+ }
85
+ }
86
+ </script>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div class="modal fade" :class="{ show: show }" :style="{ display: show ? 'block' : 'none' }">
3
+ <div class="modal-dialog">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h4 class="modal-title">{{ translations.export }}</h4>
7
+ <button type="button" class="btn-close" @click="$emit('close')"></button>
8
+ </div>
9
+ <div class="modal-body">
10
+ <form @submit.prevent="onSubmit">
11
+ <p class="text-start">{{ translations.export_notice }}</p>
12
+
13
+ <FormMultiSelect
14
+ v-model="languagesToExport"
15
+ name="languagesToExport"
16
+ :label="translations.language_to_export"
17
+ :options="localeOptions"
18
+ track-by="id"
19
+ option-label="name"
20
+ :placeholder="translations.select_language"
21
+ />
22
+
23
+ <div class="text-end">
24
+ <button class="btn btn-primary" type="submit">
25
+ <i class="fa fa-file-excel"></i>&nbsp;{{ translations.export_btn }}
26
+ </button>
27
+ </div>
28
+ </form>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ <div v-if="show" class="modal-backdrop fade show" @click="$emit('close')"></div>
34
+ </template>
35
+
36
+ <script setup>
37
+ import { ref, computed, watch } from 'vue';
38
+ import { notifyError } from '../../utils/notify.js';
39
+ import FormMultiSelect from '../../components/form/FormMultiSelect.vue';
40
+
41
+ const props = defineProps({
42
+ show: { type: Boolean, default: false },
43
+ locales: { type: [Object, Array], default: () => [] },
44
+ url: { type: String, required: true },
45
+ translations: { type: Object, default: () => ({}) },
46
+ });
47
+
48
+ const emit = defineEmits(['close']);
49
+
50
+ const localeList = computed(() => (Array.isArray(props.locales) ? props.locales : Object.values(props.locales)));
51
+
52
+ const localeOptions = computed(() => localeList.value.map((l) => ({ id: l, name: l.toUpperCase() })));
53
+
54
+ const languagesToExport = ref([]);
55
+
56
+ watch(
57
+ localeOptions,
58
+ (opts) => {
59
+ languagesToExport.value = [...opts];
60
+ },
61
+ { immediate: true },
62
+ );
63
+
64
+ function onSubmit() {
65
+ if (languagesToExport.value.length === 0) {
66
+ notifyError('Please select at least one language to export.');
67
+ return;
68
+ }
69
+ const params = new URLSearchParams();
70
+ languagesToExport.value.forEach((l) => params.append('exportLanguages[]', l.id));
71
+ window.location = props.url + '/export?' + params.toString();
72
+ emit('close');
73
+ }
74
+ </script>
@@ -0,0 +1,255 @@
1
+ <template>
2
+ <div class="modal fade" :class="{ show: show }" :style="{ display: show ? 'block' : 'none' }">
3
+ <div class="modal-dialog modal-lg">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h4 class="modal-title">{{ translations.import_title }}</h4>
7
+ <button type="button" class="btn-close" @click="onClose"></button>
8
+ </div>
9
+ <div class="modal-body">
10
+ <div v-show="currentStep === 1">
11
+ <p>{{ translations.import_notice }}</p>
12
+ <div class="row mb-3">
13
+ <div class="col-md-4 text-md-end">
14
+ <label class="col-form-label">{{ translations.upload_file }}</label>
15
+ </div>
16
+ <div class="col-md-6">
17
+ <label class="btn btn-primary btn-sm w-100 mb-0">
18
+ <span v-if="importedFile">{{ importedFile.name }}</span>
19
+ <span v-else>{{ translations.choose_file }}</span>
20
+ <input
21
+ ref="fileInput"
22
+ type="file"
23
+ class="d-none"
24
+ accept=".xlsx"
25
+ @change="handleFileUpload"
26
+ />
27
+ </label>
28
+ </div>
29
+ </div>
30
+ <div class="row mb-3">
31
+ <div class="col-md-4 text-md-end">
32
+ <label for="importLanguage" class="col-form-label">{{
33
+ translations.language_to_import
34
+ }}</label>
35
+ </div>
36
+ <div class="col-md-6">
37
+ <Multiselect
38
+ v-model="importLanguageObj"
39
+ :options="localeOptions"
40
+ track-by="id"
41
+ label="name"
42
+ :placeholder="translations.select_language"
43
+ :allow-empty="false"
44
+ :show-labels="false"
45
+ open-direction="bottom"
46
+ />
47
+ </div>
48
+ </div>
49
+ <div class="offset-md-4">
50
+ <div class="form-check">
51
+ <input
52
+ id="onlyMissingTranslations"
53
+ v-model="onlyMissing"
54
+ class="form-check-input"
55
+ type="checkbox"
56
+ />
57
+ <label class="form-check-label" for="onlyMissingTranslations">{{
58
+ translations.do_not_override
59
+ }}</label>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div v-show="currentStep === 2">
65
+ <div class="text-center mb-3">
66
+ <p>
67
+ {{ translations.conflict_found }} {{ numberOfFoundTranslations }}
68
+ {{ translations.conflict_to_import }} {{ numberOfTranslationsToReview }}
69
+ {{ translations.conflict_differ }}
70
+ </p>
71
+ </div>
72
+ <table class="table table-hover">
73
+ <thead>
74
+ <tr>
75
+ <th>{{ translations.group }}</th>
76
+ <th>{{ translations.default }}</th>
77
+ <th>{{ translations.current_value }}</th>
78
+ <th>{{ translations.imported_value }}</th>
79
+ </tr>
80
+ </thead>
81
+ <tbody>
82
+ <template v-for="(item, index) in translationsToImport" :key="index">
83
+ <tr v-if="item.has_conflict">
84
+ <td style="word-break: break-all">{{ item.group }}</td>
85
+ <td style="word-break: break-all">{{ item.default }}</td>
86
+ <td style="word-break: break-all">
87
+ <input
88
+ :id="'current-' + index + '0'"
89
+ v-model="translationsToImport[index].checkedCurrent"
90
+ type="radio"
91
+ class="form-check-input me-1"
92
+ :value="true"
93
+ :name="'current-' + index"
94
+ />
95
+ <label class="form-check-label" :for="'current-' + index + '0'">{{
96
+ item.current_value
97
+ }}</label>
98
+ </td>
99
+ <td style="word-break: break-all">
100
+ <input
101
+ :id="'current-' + index + '1'"
102
+ v-model="translationsToImport[index].checkedCurrent"
103
+ type="radio"
104
+ class="form-check-input me-1"
105
+ :value="false"
106
+ :name="'current-' + index"
107
+ />
108
+ <label class="form-check-label" :for="'current-' + index + '1'">{{
109
+ item[importLanguage.toLowerCase()]
110
+ }}</label>
111
+ </td>
112
+ </tr>
113
+ </template>
114
+ </tbody>
115
+ </table>
116
+ </div>
117
+
118
+ <div v-show="currentStep === 3">
119
+ <div class="text-center">
120
+ <p>
121
+ {{ numberOfSuccessfullyImportedTranslations }} {{ translations.successfully_imported }}
122
+ {{ numberOfSuccessfullyUpdatedTranslations }} {{ translations.successfully_updated }}
123
+ </p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ <div class="modal-footer">
128
+ <button v-if="!lastStep" type="button" class="btn btn-primary" @click.prevent="nextStep">
129
+ {{ translations.next }}
130
+ </button>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div v-if="show" class="modal-backdrop fade show" @click="onClose"></div>
136
+ </template>
137
+
138
+ <script setup>
139
+ import { ref, computed, watch } from 'vue';
140
+ import { notifyError } from '../../utils/notify.js';
141
+ import Multiselect from 'vue-multiselect';
142
+
143
+ const axios = window.axios;
144
+
145
+ const props = defineProps({
146
+ show: { type: Boolean, default: false },
147
+ locales: { type: [Object, Array], default: () => [] },
148
+ url: { type: String, required: true },
149
+ stepCount: { type: Number, default: 3 },
150
+ translations: { type: Object, default: () => ({}) },
151
+ });
152
+
153
+ const emit = defineEmits(['close', 'imported']);
154
+
155
+ const localeList = computed(() => (Array.isArray(props.locales) ? props.locales : Object.values(props.locales)));
156
+
157
+ const localeOptions = computed(() => localeList.value.map((l) => ({ id: l, name: l.toUpperCase() })));
158
+
159
+ const fileInput = ref(null);
160
+ const importedFile = ref(null);
161
+ const importLanguageObj = ref(null);
162
+ const importLanguage = computed(() => importLanguageObj.value?.id ?? '');
163
+ const onlyMissing = ref(false);
164
+ const currentStep = ref(1);
165
+ const numberOfSuccessfullyImportedTranslations = ref(0);
166
+ const numberOfSuccessfullyUpdatedTranslations = ref(0);
167
+ const numberOfFoundTranslations = ref(0);
168
+ const numberOfTranslationsToReview = ref(0);
169
+ const translationsToImport = ref(null);
170
+
171
+ const lastStep = computed(() => currentStep.value === props.stepCount);
172
+
173
+ watch(
174
+ () => props.show,
175
+ (val) => {
176
+ if (!val) resetState();
177
+ },
178
+ );
179
+
180
+ function resetState() {
181
+ currentStep.value = 1;
182
+ importedFile.value = null;
183
+ importLanguageObj.value = null;
184
+ onlyMissing.value = false;
185
+ translationsToImport.value = null;
186
+ if (fileInput.value) {
187
+ fileInput.value.value = '';
188
+ }
189
+ }
190
+
191
+ function handleFileUpload(e) {
192
+ importedFile.value = e.target.files[0];
193
+ }
194
+
195
+ function onClose() {
196
+ emit('close');
197
+ }
198
+
199
+ async function nextStep() {
200
+ if (currentStep.value === 1) {
201
+ if (!importedFile.value || !importLanguage.value) {
202
+ notifyError('The form contains invalid fields.');
203
+ return;
204
+ }
205
+
206
+ const formData = new FormData();
207
+ formData.append('fileImport', importedFile.value);
208
+ formData.append('importLanguage', importLanguage.value);
209
+ formData.append('onlyMissing', onlyMissing.value);
210
+
211
+ try {
212
+ const response = await axios.post(props.url + '/import', formData, {
213
+ headers: { 'Content-Type': 'multipart/form-data' },
214
+ });
215
+
216
+ if ('numberOfImportedTranslations' in response.data && 'numberOfUpdatedTranslations' in response.data) {
217
+ currentStep.value = 3;
218
+ numberOfSuccessfullyImportedTranslations.value = response.data.numberOfImportedTranslations;
219
+ numberOfSuccessfullyUpdatedTranslations.value = response.data.numberOfUpdatedTranslations;
220
+ emit('imported');
221
+ } else {
222
+ currentStep.value = 2;
223
+ translationsToImport.value = response.data;
224
+ numberOfFoundTranslations.value = Object.keys(response.data).length;
225
+ numberOfTranslationsToReview.value = translationsToImport.value.filter((i) => i.has_conflict).length;
226
+ }
227
+ } catch (error) {
228
+ const msg = error.response?.data;
229
+ if (msg === 'Wrong syntax in your import') notifyError('Wrong syntax in your import.');
230
+ else if (msg === 'Unsupported file type') notifyError('Unsupported file type.');
231
+ else notifyError();
232
+ }
233
+ } else if (currentStep.value === 2) {
234
+ for (let i = 0; i < translationsToImport.value.length; i++) {
235
+ if (translationsToImport.value[i].checkedCurrent) {
236
+ translationsToImport.value[i][importLanguage.value.toLowerCase()] =
237
+ translationsToImport.value[i].current_value;
238
+ }
239
+ }
240
+
241
+ try {
242
+ const response = await axios.post(props.url + '/import/conflicts', {
243
+ importLanguage: importLanguage.value,
244
+ resolvedTranslations: translationsToImport.value,
245
+ });
246
+ currentStep.value = 3;
247
+ numberOfSuccessfullyImportedTranslations.value = response.data.numberOfImportedTranslations;
248
+ numberOfSuccessfullyUpdatedTranslations.value = response.data.numberOfUpdatedTranslations;
249
+ emit('imported');
250
+ } catch {
251
+ notifyError();
252
+ }
253
+ }
254
+ }
255
+ </script>