@weni/unnnic-system 2.6.1-alpha.2 → 2.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weni/unnnic-system",
3
- "version": "2.6.1-alpha.2",
3
+ "version": "2.6.2",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -82,4 +82,4 @@
82
82
  "vitest": "^1.6.0",
83
83
  "vue-eslint-parser": "^9.4.2"
84
84
  }
85
- }
85
+ }
@@ -19,7 +19,6 @@
19
19
  :style="{ position: 'absolute' }"
20
20
  class="rotation"
21
21
  :next="next"
22
- data-testid="icon-loading"
23
22
  />
24
23
 
25
24
  <UnnnicIcon
@@ -30,7 +29,6 @@
30
29
  :class="{ 'unnnic-button__icon-left': hasText }"
31
30
  :style="{ visibility: loading ? 'hidden' : null }"
32
31
  :next="next"
33
- data-testid="icon-left"
34
32
  />
35
33
 
36
34
  <UnnnicIcon
@@ -40,7 +38,6 @@
40
38
  :style="{ visibility: loading ? 'hidden' : null }"
41
39
  :size="iconSize"
42
40
  :next="next"
43
- data-testid="icon-center"
44
41
  />
45
42
 
46
43
  <span
@@ -59,7 +56,6 @@
59
56
  :class="{ 'unnnic-button__icon-right': hasText }"
60
57
  :style="{ visibility: loading ? 'hidden' : null }"
61
58
  :next="next"
62
- data-testid="icon-loading"
63
59
  />
64
60
  </button>
65
61
  </template>
@@ -68,7 +64,6 @@
68
64
  import UnnnicIcon from '../Icon.vue';
69
65
 
70
66
  export default {
71
- name: 'UnnnicButton',
72
67
  components: {
73
68
  UnnnicIcon,
74
69
  },
@@ -10,7 +10,6 @@
10
10
  'material-symbols-rounded--filled': filled,
11
11
  },
12
12
  ]"
13
- data-testid="material-icon"
14
13
  @click="onClick"
15
14
  @mousedown="$emit('mousedown')"
16
15
  @mouseup="$emit('mouseup')"
@@ -28,7 +27,6 @@
28
27
  lineHeight ? `unnnic-icon__line-height--${lineHeight}` : '',
29
28
  scheme ? `unnnic-icon-scheme--${scheme}` : '',
30
29
  ]"
31
- data-testid="old-map-icons"
32
30
  @click="onClick"
33
31
  @mousedown="$emit('mousedown')"
34
32
  @mouseup="$emit('mouseup')"
@@ -1,12 +1,5 @@
1
1
  <template>
2
2
  <div class="unnnic-import-card">
3
- <UnnnicIcon
4
- class="unnnic-import-card__file-icon"
5
- size="md"
6
- scheme="weni-600"
7
- :icon="fileIcon"
8
- />
9
-
10
3
  <div class="unnnic-import-card__data">
11
4
  <div
12
5
  v-if="isImporting"
@@ -58,13 +51,12 @@
58
51
  />
59
52
  </UnnnicButton>
60
53
 
61
- <UnnnicIcon
54
+ <UnnnicButton
62
55
  v-if="canDelete"
63
56
  class="unnnic-import-card__buttons__delete"
64
- size="sm"
65
- scheme="neutral-cloudy"
66
- icon="delete"
67
- clickable
57
+ size="small"
58
+ :iconCenter="`close-1`"
59
+ type="warning"
68
60
  @click="emitDeletion"
69
61
  />
70
62
  </div>
@@ -73,13 +65,11 @@
73
65
 
74
66
  <script>
75
67
  import UnnnicButton from '../Button/Button.vue';
76
- import UnnnicIcon from '../Icon.vue';
77
68
 
78
69
  export default {
79
70
  name: 'ImportCard',
80
71
  components: {
81
72
  UnnnicButton,
82
- UnnnicIcon,
83
73
  },
84
74
  props: {
85
75
  title: {
@@ -115,22 +105,6 @@ export default {
115
105
  default: 'upload-bottom-1',
116
106
  },
117
107
  },
118
- computed: {
119
- fileIcon() {
120
- const extension = this.title.slice(this.title.lastIndexOf('.') + 1);
121
-
122
- return (
123
- {
124
- pdf: 'picture_as_pdf',
125
- txt: 'text_snippet',
126
- xls: 'table',
127
- xlsx: 'table',
128
- doc: 'draft',
129
- docx: 'draft',
130
- }[extension] || 'draft'
131
- );
132
- },
133
- },
134
108
  methods: {
135
109
  importFile() {
136
110
  this.$refs.file.click();
@@ -159,23 +133,15 @@ export default {
159
133
 
160
134
  .unnnic-import-card {
161
135
  display: flex;
162
- column-gap: $unnnic-spacing-xs;
163
- align-items: center;
164
136
 
165
137
  border: $unnnic-border-width-thinner solid $unnnic-color-neutral-soft;
166
- border-radius: $unnnic-border-radius-sm;
138
+ border-radius: $unnnic-border-radius-md;
167
139
 
168
- background-color: $unnnic-color-neutral-white;
169
- padding: $unnnic-spacing-xs - $unnnic-border-width-thinner;
170
-
171
- &__file-icon {
172
- user-select: none;
173
- margin: $unnnic-spacing-xs;
174
- }
140
+ background-color: $unnnic-color-background-snow;
141
+ padding: $unnnic-squish-md;
175
142
 
176
143
  &__data {
177
144
  align-self: center;
178
- overflow: hidden;
179
145
 
180
146
  flex: 1;
181
147
  gap: $unnnic-spacing-stack-nano;
@@ -183,14 +149,10 @@ export default {
183
149
  font-family: $unnnic-font-family-secondary;
184
150
 
185
151
  &__title {
186
- color: $unnnic-color-neutral-dark;
152
+ color: $unnnic-color-neutral-darkest;
187
153
  font-size: $unnnic-font-size-body-gt;
188
154
  line-height: $unnnic-font-size-body-gt + $unnnic-line-height-md;
189
- font-weight: $unnnic-font-weight-regular;
190
-
191
- white-space: nowrap;
192
- text-overflow: ellipsis;
193
- overflow: hidden;
155
+ font-weight: $unnnic-font-weight-bold;
194
156
  }
195
157
 
196
158
  &__subtitle {
@@ -219,9 +181,8 @@ export default {
219
181
  &__buttons {
220
182
  display: flex;
221
183
  gap: $unnnic-spacing-inline-xs;
222
- align-items: center;
223
184
 
224
- margin-right: $unnnic-spacing-xs;
185
+ margin-left: $unnnic-spacing-inline-xs;
225
186
 
226
187
  align-self: center;
227
188
 
@@ -239,10 +200,6 @@ export default {
239
200
  }
240
201
  }
241
202
  }
242
-
243
- &__delete {
244
- user-select: none;
245
- }
246
203
  }
247
204
  }
248
205
  </style>
@@ -1,17 +1,64 @@
1
1
  <template>
2
2
  <div>
3
- <UnnnicDropArea
4
- v-model:currentFiles="currentFiles"
5
- :acceptMultiple="acceptMultiple"
6
- :supportedFormats="supportedFormats"
7
- :maxFileSize="maxFileSize"
8
- :shouldReplace="shouldReplace"
9
- :maximumUploads="maximumUploads"
10
- :subtitle="subtitle"
11
- rato="oi"
12
- @file-change="$emit('fileChange', $event)"
13
- />
3
+ <div
4
+ ref="dropzone"
5
+ :class="{
6
+ 'unnnic-upload-area__dropzone': true,
7
+ 'unnnic-upload-area__dropzone__is-dragover': isDragging,
8
+ 'unnnic-upload-area__dropzone__has-error': hasError,
9
+ }"
10
+ @dragenter.stop.prevent="dragenter"
11
+ @dragover.stop.prevent="dragover"
12
+ @dragleave.stop.prevent="dragleave"
13
+ @dragend.stop.prevent="dragend"
14
+ @drop.stop.prevent="drop"
15
+ @click="() => $refs.file.click()"
16
+ >
17
+ <UnnnicIcon
18
+ class="unnnic-upload-area__dropzone__icon"
19
+ icon="upload-bottom-1"
20
+ :scheme="hasError ? 'feedback-red' : 'brand-weni'"
21
+ size="xl"
22
+ />
14
23
 
24
+ <div class="unnnic-upload-area__dropzone__content">
25
+ <span class="unnnic-upload-area__dropzone__content__title">
26
+ {{ $t('upload_area.title.text') }}
27
+ <span
28
+ :class="`unnnic-upload-area__dropzone__content__title__${
29
+ hasError ? 'error' : 'search'
30
+ }`"
31
+ >
32
+ {{ $t('upload_area.title.highlight') }}
33
+ </span>
34
+ </span>
35
+ <span
36
+ :class="[
37
+ 'unnnic-upload-area__dropzone__content__subtitle',
38
+ {
39
+ 'unnnic-upload-area__dropzone__content__subtitle__error':
40
+ hasError,
41
+ },
42
+ ]"
43
+ :title="formattedSupportedFormats"
44
+ >
45
+ {{
46
+ subtitle ||
47
+ `${$t(
48
+ `upload_area${hasError ? '.invalid' : ''}.subtitle`,
49
+ )} ${formattedSupportedFormats}`
50
+ }}
51
+ </span>
52
+ </div>
53
+ <input
54
+ ref="file"
55
+ type="file"
56
+ :accept="supportedFormats"
57
+ :multiple="acceptMultiple"
58
+ style="display: none"
59
+ @input="handleFileChange"
60
+ />
61
+ </div>
15
62
  <div
16
63
  v-if="currentFiles.length > 0"
17
64
  class="unnnic-upload-area__cards"
@@ -35,13 +82,15 @@
35
82
  </template>
36
83
 
37
84
  <script>
38
- import UnnnicDropArea from '../DropArea/DropArea.vue';
85
+ import mime from 'mime';
86
+
87
+ import UnnnicIcon from '../Icon.vue';
39
88
  import UnnnicImportCard from '../ImportCard/ImportCard.vue';
40
89
 
41
90
  export default {
42
91
  name: 'UnnnicUploadArea',
43
92
  components: {
44
- UnnnicDropArea,
93
+ UnnnicIcon,
45
94
  UnnnicImportCard,
46
95
  },
47
96
  model: {
@@ -95,26 +144,156 @@ export default {
95
144
  default: '',
96
145
  },
97
146
  },
98
-
99
- emits: ['fileChange'],
100
-
101
147
  data() {
102
148
  return {
149
+ hasError: false,
150
+ isDragging: false,
151
+ dragEnterCounter: 0, // to handle dragenter/dragleave on child elements
103
152
  currentFiles: this.files,
104
153
  };
105
154
  },
155
+ computed: {
156
+ formattedSupportedFormats() {
157
+ const formats = this.supportedFormats
158
+ .split(',')
159
+ .map((format) => format.toUpperCase());
106
160
 
161
+ return formats.join(', ');
162
+ },
163
+ },
107
164
  watch: {
108
165
  files(newValue) {
109
166
  this.currentFiles = newValue;
110
167
  },
111
168
  },
112
-
113
169
  methods: {
170
+ dragenter() {
171
+ this.dragEnterCounter += 1;
172
+ this.isDragging = true;
173
+ },
174
+ dragover() {
175
+ this.isDragging = true;
176
+ },
177
+ dragleave() {
178
+ this.dragEnterCounter -= 1;
179
+ if (this.dragEnterCounter === 0) {
180
+ this.isDragging = false;
181
+ }
182
+ },
183
+ dragend() {
184
+ this.isDragging = false;
185
+ },
186
+ drop(event) {
187
+ this.isDragging = false;
188
+
189
+ const { files } = event.dataTransfer;
190
+
191
+ if (this.validateFiles(files)) {
192
+ this.addFiles(files);
193
+ }
194
+ },
195
+ handleFileChange(event) {
196
+ const { files } = event.target;
197
+
198
+ if (this.validateFiles(files)) {
199
+ this.addFiles(files);
200
+ }
201
+ this.$refs.file.value = '';
202
+ },
203
+ validateFiles(files) {
204
+ if (!this.acceptMultiple && files.length > 1) {
205
+ this.setErrorState();
206
+ return false;
207
+ }
208
+
209
+ if (!this.validFormat(files)) {
210
+ this.setErrorState();
211
+ return false;
212
+ }
213
+
214
+ if (!this.validSize(files)) {
215
+ this.setErrorState();
216
+ return false;
217
+ }
218
+
219
+ return true;
220
+ },
221
+ validFormat(files) {
222
+ if (this.supportedFormats === '*') {
223
+ return true;
224
+ }
225
+
226
+ const formats = this.supportedFormats
227
+ .split(',')
228
+ .map((format) => format.trim());
229
+
230
+ const isValid = Array.from(files).find((file) => {
231
+ const fileName = file.name.toLowerCase();
232
+ const fileType = file.type.toLowerCase();
233
+ const fileExtension = `.${fileName.split('.').pop()}`;
234
+
235
+ const isValidFileExtension = formats.includes(fileExtension);
236
+ const isValidFileType = fileType === mime.getType(fileName);
237
+
238
+ return isValidFileExtension && isValidFileType;
239
+ });
240
+
241
+ return isValid;
242
+ },
243
+
244
+ validSize(files) {
245
+ if (!this.maxFileSize) {
246
+ return true;
247
+ }
248
+
249
+ const isValid = Array.from(files).find((file) => {
250
+ const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);
251
+
252
+ return sizeInMB <= this.maxFileSize;
253
+ });
254
+
255
+ return isValid;
256
+ },
257
+
258
+ setErrorState() {
259
+ this.hasError = true;
260
+
261
+ setTimeout(() => {
262
+ this.hasError = false;
263
+ }, 5000);
264
+ },
265
+
114
266
  emitFileChange() {
115
267
  this.$emit('fileChange', this.currentFiles);
116
268
  },
117
269
 
270
+ addFiles(files) {
271
+ let totalLength = files.length;
272
+
273
+ if (!this.shouldReplace) {
274
+ totalLength += this.currentFiles.length;
275
+ }
276
+
277
+ if (totalLength > this.maximumUploads) {
278
+ this.setErrorState();
279
+ return;
280
+ }
281
+
282
+ const validFiles = Array.from(files).filter((file) => {
283
+ if (this.validFormat([file]) && this.validSize([file])) {
284
+ return true;
285
+ }
286
+ return false;
287
+ });
288
+
289
+ if (this.shouldReplace) {
290
+ this.currentFiles = validFiles;
291
+ } else {
292
+ this.currentFiles = this.currentFiles.concat(validFiles);
293
+ }
294
+ this.emitFileChange();
295
+ },
296
+
118
297
  modifyFile(index, file) {
119
298
  this.currentFiles.splice(index, 1, file);
120
299
  this.emitFileChange();
@@ -124,6 +303,15 @@ export default {
124
303
  this.currentFiles.splice(index, 1);
125
304
  this.emitFileChange();
126
305
  },
306
+
307
+ checkDragAndDropSupport() {
308
+ const { dropzone } = this.$refs;
309
+ return (
310
+ 'FileReader' in window &&
311
+ ('draggable' in dropzone ||
312
+ ('ondragstart' in dropzone && 'ondrop' in dropzone))
313
+ );
314
+ },
127
315
  },
128
316
  };
129
317
  </script>
@@ -131,7 +319,73 @@ export default {
131
319
  <style lang="scss" scoped>
132
320
  @import '../../assets/scss/unnnic.scss';
133
321
 
322
+ @function borderDashed($color) {
323
+ $colorString: unquote('' + $color);
324
+ $cleanColor: str-slice($colorString, 2);
325
+ @return url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23#{$cleanColor}' stroke-width='4' stroke-dasharray='4%2c 12' stroke-dashoffset='9' stroke-linecap='square'/%3e%3c/svg%3e");
326
+ }
327
+
134
328
  .unnnic-upload-area {
329
+ &__dropzone {
330
+ border-radius: $unnnic-border-radius-lg;
331
+ background-color: $unnnic-color-background-carpet;
332
+ padding: $unnnic-spacing-inset-lg;
333
+
334
+ // Dashed border with increased dashes spacing and color neutral clean
335
+ background-image: borderDashed($unnnic-color-neutral-cleanest);
336
+
337
+ &__has-error {
338
+ background-image: borderDashed($unnnic-color-feedback-red);
339
+ }
340
+
341
+ &__is-dragover {
342
+ background-color: $unnnic-color-background-sky;
343
+ background-image: borderDashed($unnnic-color-brand-weni);
344
+ }
345
+
346
+ &__icon {
347
+ display: block;
348
+ margin: 0 auto;
349
+
350
+ margin-bottom: $unnnic-spacing-stack-sm;
351
+ }
352
+
353
+ &__content {
354
+ display: flex;
355
+ flex-direction: column;
356
+ gap: $unnnic-spacing-stack-nano;
357
+
358
+ text-align: center;
359
+ font-family: $unnnic-font-family-secondary;
360
+
361
+ &__title {
362
+ color: $unnnic-color-neutral-darkest;
363
+ font-weight: $unnnic-font-weight-bold;
364
+ font-size: $unnnic-font-size-body-gt;
365
+ line-height: $unnnic-font-size-body-gt + $unnnic-line-height-md;
366
+
367
+ &__search {
368
+ color: $unnnic-color-brand-weni;
369
+ }
370
+
371
+ &__error {
372
+ color: $unnnic-color-feedback-red;
373
+ }
374
+ }
375
+
376
+ &__subtitle {
377
+ color: $unnnic-color-neutral-cloudy;
378
+ font-weight: $unnnic-font-weight-regular;
379
+ font-size: $unnnic-font-size-body-md;
380
+ line-height: $unnnic-font-size-body-md + $unnnic-line-height-md;
381
+
382
+ &__error {
383
+ color: $unnnic-color-feedback-red;
384
+ }
385
+ }
386
+ }
387
+ }
388
+
135
389
  &__cards {
136
390
  margin-top: $unnnic-spacing-stack-md;
137
391
  display: flex;
@@ -139,4 +393,11 @@ export default {
139
393
  gap: $unnnic-spacing-stack-xs;
140
394
  }
141
395
  }
396
+
397
+ .unnnic-upload-area__dropzone__dragndrop,
398
+ .unnnic-upload-area__dropzone__uploading,
399
+ .unnnic-upload-area__dropzone__success,
400
+ .unnnic-upload-area__dropzone__error {
401
+ display: none;
402
+ }
142
403
  </style>
@@ -54,7 +54,6 @@ import Switch from './Switch/Switch.vue';
54
54
  import Slider from './Slider/Slider.vue';
55
55
  import DataArea from './DataArea/DataArea.vue';
56
56
  import Pagination from './Pagination/Pagination.vue';
57
- import DropArea from './DropArea/DropArea.vue';
58
57
  import UploadArea from './UploadArea/UploadArea.vue';
59
58
  import ImportCard from './ImportCard/ImportCard.vue';
60
59
  import DateFilter from './DateFilter/DateFilter.vue';
@@ -144,7 +143,6 @@ export const components = {
144
143
  unnnicSlider: Slider,
145
144
  unnnicDataArea: DataArea,
146
145
  unnnicPagination: Pagination,
147
- unnnicDropArea: DropArea,
148
146
  unnnicUploadArea: UploadArea,
149
147
  unnnicImportCard: ImportCard,
150
148
  unnnicDateFilter: DateFilter,
@@ -233,7 +231,6 @@ export const unnnicSwitch = Switch;
233
231
  export const unnnicSlider = Slider;
234
232
  export const unnnicDataArea = DataArea;
235
233
  export const unnnicPagination = Pagination;
236
- export const unnnicDropArea = DropArea;
237
234
  export const unnnicUploadArea = UploadArea;
238
235
  export const unnnicImportCard = ImportCard;
239
236
  export const unnnicDateFilter = DateFilter;
@@ -1,94 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { mount } from '@vue/test-utils';
3
-
4
- import Button from '../Button.vue';
5
-
6
- const createWrapper = (props) => {
7
- return mount(Button, { props });
8
- };
9
-
10
- describe('Button', () => {
11
- it('should render text and default props', () => {
12
- const wrapper = createWrapper({ text: 'Button' });
13
- expect(wrapper.text()).contain('Button');
14
- expect(wrapper.classes()).toContain('unnnic-button');
15
- expect(wrapper.classes()).toContain('unnnic-button--size-large');
16
- expect(wrapper.classes()).toContain('unnnic-button--primary');
17
- });
18
-
19
- it('should render left icon and text', () => {
20
- const wrapper = createWrapper({ iconLeft: 'search-1', text: 'Search' });
21
-
22
- const buttonChildren = wrapper.findComponent({
23
- name: 'UnnnicButton',
24
- }).element.children;
25
-
26
- expect(
27
- buttonChildren[0].classList.contains('unnnic-button__icon-left'),
28
- ).toBe(true);
29
- expect(buttonChildren[1].classList.contains('unnnic-button__label')).toBe(
30
- true,
31
- );
32
- });
33
-
34
- it('should render right icon', () => {
35
- const wrapper = createWrapper({ iconRight: 'search-1', text: 'Search' });
36
-
37
- const buttonChildren = wrapper.findComponent({
38
- name: 'UnnnicButton',
39
- }).element.children;
40
-
41
- expect(buttonChildren[0].classList.contains('unnnic-button__label')).toBe(
42
- true,
43
- );
44
-
45
- expect(
46
- buttonChildren[1].classList.contains('unnnic-button__icon-right'),
47
- ).toBe(true);
48
- });
49
-
50
- it('should render button only icon and float variation', async () => {
51
- const wrapper = createWrapper({ iconCenter: 'search-1' });
52
-
53
- expect(wrapper.classes()).toContain('unnnic-button--icon-on-center');
54
-
55
- const buttonChildren = wrapper.findComponent({
56
- name: 'UnnnicButton',
57
- }).element.children;
58
-
59
- expect(buttonChildren[0].classList.contains('unnnic-icon')).toBe(true);
60
- expect(wrapper.text()).toBe('');
61
-
62
- await wrapper.setProps({ float: true, size: 'extra-large' });
63
-
64
- expect(wrapper.classes()).toContain('unnnic-button--float');
65
- });
66
-
67
- it('should emit click event', async () => {
68
- const wrapper = createWrapper({ text: 'Button' });
69
- await wrapper.trigger('click');
70
-
71
- expect(wrapper.emitted('click')).toBeTruthy();
72
- });
73
-
74
- it('should disable click event', async () => {
75
- const wrapper = createWrapper({ text: 'Button', disabled: true });
76
- await wrapper.trigger('click');
77
- expect(wrapper.emitted('click')).toBe(undefined);
78
- });
79
-
80
- it('should show loading variation and disable click', async () => {
81
- const wrapper = createWrapper({ loading: true });
82
- await wrapper.trigger('click');
83
- expect(wrapper.emitted('click')).toBe(undefined);
84
-
85
- const loadingIcon = wrapper.findComponent('[data-testid="icon-loading"]');
86
- expect(loadingIcon.exists()).toBe(true);
87
- });
88
- it('should show errors because invalid props', () => {
89
- const invalidSize = () => createWrapper({ size: 'invalid-size' });
90
- expect(invalidSize).toThrow(Error);
91
- const invalidType = () => createWrapper({ type: 'invalid-type' });
92
- expect(invalidType).toThrow(Error);
93
- });
94
- });