@farm-investimentos/front-mfe-components-vue3 1.1.1 → 1.1.4

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.
@@ -1,37 +1,66 @@
1
1
  <template>
2
- <section ref="container" id="droparea-container">
2
+ <section
3
+ ref="container"
4
+ id="droparea-container"
5
+ >
3
6
  <input
4
7
  type="file"
5
8
  name="file"
6
9
  multiple
7
10
  ref="upload"
8
- :accept="acceptedFileTypes"
9
- @change="fileChange($event.target.files)"
11
+ :accept="props.acceptedFileTypes"
12
+ @change="handleFileChange"
10
13
  />
11
- <div v-if="!hasFiles" class="selectfile-container">
12
- <farm-icon class="upload-icon" color="primary" size="lg" @click="addMoreFiles">
14
+
15
+ <div
16
+ v-if="!hasFiles"
17
+ class="selectfile-container"
18
+ >
19
+ <farm-icon
20
+ class="upload-icon"
21
+ color="primary"
22
+ size="lg"
23
+ @click="addMoreFiles"
24
+ >
13
25
  cloud-upload-outline
14
26
  </farm-icon>
15
- <farm-subtitle :type="2" variation="regular" color="primary">
27
+ <farm-subtitle
28
+ :type="2"
29
+ variation="regular"
30
+ color="primary"
31
+ >
16
32
  Arraste e solte o arquivo <br />
17
33
  ou clique aqui
18
34
  </farm-subtitle>
19
35
  </div>
20
36
 
21
37
  <ul
22
- v-if="downloadFiles.length"
38
+ v-if="props.downloadFiles.length"
23
39
  class="listFilesStyled listFilesStyled--download"
24
40
  :class="{
25
41
  'listFilesStyled--margin-bottom': files.length === 0,
26
42
  }"
27
43
  >
28
- <li class="itemFilesStyled" v-for="file in downloadFiles" :key="file.id">
44
+ <li
45
+ class="itemFilesStyled"
46
+ v-for="file in props.downloadFiles"
47
+ :key="file.id"
48
+ >
29
49
  <div class="itemFilesContentStyled">
30
50
  <div class="fileDocumentStyled">
31
- <farm-icon color="black" variation="50" size="sm">attachment</farm-icon>
51
+ <farm-icon
52
+ color="black"
53
+ variation="50"
54
+ size="sm"
55
+ >
56
+ attachment
57
+ </farm-icon>
32
58
  </div>
33
59
  <div>
34
- <farm-bodysmall color="black" color-variation="50">
60
+ <farm-bodysmall
61
+ color="black"
62
+ color-variation="50"
63
+ >
35
64
  Arquivo selecionado: {{ file.name }} ({{ sizeOf(file.size) }})
36
65
  </farm-bodysmall>
37
66
  </div>
@@ -45,24 +74,42 @@
45
74
  title="Baixar Arquivo"
46
75
  @click.prevent="onDownload(file.id)"
47
76
  >
48
- <farm-icon class="download-button__icon mr-sm-3" size="md"
49
- >download</farm-icon
77
+ <farm-icon
78
+ class="download-button__icon mr-sm-3"
79
+ size="md"
50
80
  >
51
-
81
+ download
82
+ </farm-icon>
52
83
  <span class="download-button__text">Baixar Arquivo</span>
53
84
  </farm-btn>
54
85
  </div>
55
86
  </li>
56
87
  </ul>
57
88
 
58
- <ul class="listFilesStyled" v-if="files.length > 0">
59
- <li class="itemFilesStyled" v-for="(file, index) in files" :key="index">
89
+ <ul
90
+ class="listFilesStyled"
91
+ v-if="files.length > 0"
92
+ >
93
+ <li
94
+ class="itemFilesStyled"
95
+ v-for="(file, index) in files"
96
+ :key="index"
97
+ >
60
98
  <div class="itemFilesContentStyled">
61
99
  <div class="fileDocumentStyled">
62
- <farm-icon color="black" variation="50" size="sm">attachment</farm-icon>
100
+ <farm-icon
101
+ color="black"
102
+ variation="50"
103
+ size="sm"
104
+ >
105
+ attachment
106
+ </farm-icon>
63
107
  </div>
64
108
  <div>
65
- <farm-bodysmall color="black" color-variation="50">
109
+ <farm-bodysmall
110
+ color="black"
111
+ color-variation="50"
112
+ >
66
113
  Arquivo selecionado: {{ file.name }} ({{ sizeOf(file.size) }})
67
114
  </farm-bodysmall>
68
115
  </div>
@@ -78,10 +125,17 @@
78
125
  >
79
126
  <farm-icon size="md">close</farm-icon>
80
127
  </farm-btn>
81
- <farm-icon size="md" color="neutral" v-if="file.success">check</farm-icon>
128
+ <farm-icon
129
+ size="md"
130
+ color="neutral"
131
+ v-if="file.success === true"
132
+ >
133
+ check
134
+ </farm-icon>
82
135
  </div>
83
136
  </li>
84
137
  </ul>
138
+
85
139
  <farm-btn
86
140
  v-if="canAddMoreFiles && hasFiles"
87
141
  outlined
@@ -94,169 +148,102 @@
94
148
  </farm-btn>
95
149
  </section>
96
150
  </template>
97
- <script lang="ts">
98
- import { PropType } from 'vue';
151
+
152
+ <script setup lang="ts">
153
+ import { ref, computed, watch, defineEmits } from 'vue';
99
154
 
100
155
  import { sizeOf } from '@farm-investimentos/front-mfe-libs-ts';
101
156
 
102
- export type DownloadFiles = {
157
+ type DownloadFiles = {
103
158
  id: number | string;
104
159
  name: string;
105
160
  size: number;
106
161
  };
107
162
 
108
- export default {
109
- name: 'farm-multiple-filepicker',
110
-
111
- props: {
112
- /*
113
- * Accepted file types
114
- */
115
- acceptedFileTypes: {
116
- type: String,
117
- default: '*',
118
- },
119
- /**
120
- * Max file size (in MB)
121
- */
122
- maxFileSize: {
123
- default: 0,
124
- },
125
- /**
126
- * Max files number
127
- */
128
- maxFilesNumber: {
129
- type: Number,
130
- default: 0,
131
- },
132
- /**
133
- * File List from files who could be download
134
- */
135
- downloadFiles: {
136
- type: Array as PropType<Array<DownloadFiles>>,
137
- default: () => [],
138
- },
139
- },
140
- data() {
141
- return {
142
- dropArea: null,
143
- files: [],
144
- sizeOf,
145
- };
146
- },
147
- computed: {
148
- hasFiles(): boolean {
149
- return this.files.length > 0 || this.downloadFiles.length > 0;
150
- },
151
- canAddMoreFiles(): boolean {
152
- return this.filesLength < this.maxFilesNumber;
153
- },
154
- filesLength(): number {
155
- return this.files.length + this.downloadFiles.length;
156
- },
157
- disabledButton(): boolean {
158
- if (this.maxFilesNumber === 0) return false;
159
- return this.filesLength >= this.maxFilesNumber;
160
- },
161
- },
162
- mounted() {
163
- this.dropArea = this.$refs.container;
164
- this.addListeners();
165
- },
166
- methods: {
167
- addMoreFiles(): void {
168
- this.$refs.container.querySelector('input').click();
169
- },
170
- reset(): void {
171
- this.$refs.container.querySelector('input').value = '';
172
- this.files = [];
173
- },
174
- fileChange(fileList): void {
175
- this.files.push(...fileList);
176
- },
177
- handlerFunctionHighlight(): void {
178
- this.dropArea.classList.add('highlight');
179
- },
180
- handlerFunctionUnhighlight(): void {
181
- this.dropArea.classList.remove('highlight');
182
- },
183
- addListeners(): void {
184
- this.dropArea.addEventListener('dragenter', this.handlerFunctionHighlight, false);
185
- this.dropArea.addEventListener('dragleave', this.handlerFunctionUnhighlight, false);
186
- this.dropArea.addEventListener('dragover', this.handlerFunctionHighlight, false);
187
- this.dropArea.addEventListener('drop', this.handlerFunctionUnhighlight, false);
188
- },
189
-
190
- remove(index: number): void {
191
- if (this.files.length === 1) this.reset();
192
- this.files.splice(index, 1);
193
- },
194
-
195
- onDownload(id: number): void {
196
- this.$emit('onDownload', id);
197
- },
198
- },
199
- watch: {
200
- files(newValue) {
201
- if (newValue.length === 0 && this.downloadFiles.length === 0) {
202
- this.$emit('onFileChange', newValue);
203
- return;
204
- }
205
- const invalidTypeArray = newValue.filter(file => {
206
- const type = file.type;
207
-
208
- return (
209
- this.acceptedFileTypes !== '*' &&
210
- (this.acceptedFileTypes.indexOf(file.type) === -1 || !type)
211
- );
212
- });
213
-
214
- if (invalidTypeArray.length > 0) {
215
- const validTypeArray = newValue.filter(file => {
216
- const type = file.type;
217
- if (this.acceptedFileTypes.indexOf(file.type) === -1 || !type) {
218
- return false;
219
- }
220
- return true;
221
- });
222
- this.$emit('onInvalidFiles', [...invalidTypeArray]);
223
- this.files = validTypeArray;
224
- return;
225
- }
163
+ type ExtendedFile = {
164
+ name: string;
165
+ size: number;
166
+ success?: boolean;
167
+ };
226
168
 
227
- if (
228
- !!this.maxFilesNumber &&
229
- newValue.length + this.downloadFiles.length > this.maxFilesNumber
230
- ) {
231
- this.files = newValue.slice(0, this.maxFilesNumber - this.downloadFiles.length);
232
- this.$emit('onMaxFilesNumberWarning');
233
- return;
234
- }
169
+ defineOptions({
170
+ name: 'farm-multiple-filepicker',
171
+ });
172
+
173
+ const props = defineProps({
174
+ acceptedFileTypes: { type: String, default: '*' },
175
+ maxFileSize: { type: Number, default: 0 },
176
+ maxFilesNumber: { type: Number, default: 0 },
177
+ downloadFiles: { type: Array as () => DownloadFiles[], default: () => [] },
178
+ });
179
+
180
+ const emit = defineEmits([
181
+ 'onFileChange',
182
+ 'onInvalidFiles',
183
+ 'onMaxFilesNumberWarning',
184
+ 'onDownload',
185
+ 'onReset',
186
+ ]);
187
+
188
+ const files = ref<ExtendedFile[]>([]);
189
+ const dropArea = ref<HTMLElement | null>(null);
190
+ const container = ref<HTMLElement | null>(null);
191
+
192
+ const hasFiles = computed(() => files.value.length > 0 || props.downloadFiles.length > 0);
193
+ const canAddMoreFiles = computed(() => files.value.length < props.maxFilesNumber);
194
+ const disabledButton = computed(() =>
195
+ props.maxFilesNumber === 0 ? false : files.value.length >= props.maxFilesNumber
196
+ );
197
+
198
+ const addMoreFiles = () => {
199
+ const input = dropArea.value?.querySelector('input[type="file"]') as HTMLInputElement;
200
+ if (input) input.click();
201
+ };
235
202
 
236
- if (this.maxFileSize > 0) {
237
- const files = newValue.filter(file => {
238
- const sizeInMB = file.size / (1024 * 1024);
239
- if (sizeInMB > this.maxFileSize) return true;
203
+ // const reset = () => {
204
+ // const input = container.value?.querySelector('input[type="file"]') as HTMLInputElement;
205
+ // if (input) input.value = '';
206
+ // files.value = [];
207
+ // emit('onReset');
208
+ // };
240
209
 
241
- return false;
242
- });
243
- if (files.length > 0) {
244
- this.files = newValue.filter(file => {
245
- const sizeInMB = file.size / (1024 * 1024);
246
- if (sizeInMB < this.maxFileSize) return true;
210
+ const remove = (index: number) => {
211
+ files.value.splice(index, 1);
212
+ emit('onFileChange', files.value);
213
+ };
247
214
 
248
- return false;
249
- });
215
+ const handleFileChange = (event: Event) => {
216
+ const target = event.target as HTMLInputElement;
217
+ const fileList = target.files ? Array.from(target.files) : [];
218
+ if (fileList.length) {
219
+ files.value = [...files.value, ...fileList];
220
+ emit('onFileChange', files.value);
221
+ }
222
+ };
250
223
 
251
- this.$emit('onMaxFileSizeWarning');
252
- return;
253
- }
254
- }
255
- this.$emit('onFileChange', this.files);
256
- },
257
- },
224
+ const onDownload = (id: number | string) => {
225
+ emit('onDownload', id);
258
226
  };
227
+
228
+ watch(files, newValue => {
229
+ if (newValue.length > props.maxFilesNumber && props.maxFilesNumber > 0) {
230
+ files.value = newValue.slice(0, props.maxFilesNumber);
231
+ emit('onMaxFilesNumberWarning');
232
+ }
233
+
234
+ if (props.maxFileSize > 0) {
235
+ const validFiles = newValue.filter(file => file.size / (1024 * 1024) <= props.maxFileSize);
236
+ if (validFiles.length !== newValue.length) {
237
+ emit(
238
+ 'onInvalidFiles',
239
+ newValue.filter(file => !validFiles.includes(file))
240
+ );
241
+ files.value = validFiles;
242
+ }
243
+ }
244
+ });
259
245
  </script>
246
+
260
247
  <style scoped lang="scss">
261
248
  @import './MultipleFilePicker.scss';
262
249
  </style>
@@ -1,184 +1,102 @@
1
- import { shallowMount } from '@vue/test-utils';
1
+ import { mount } from '@vue/test-utils';
2
2
 
3
3
  import MultipleFilePicker from '../index';
4
4
 
5
5
  describe('MultipleFilePicker component', () => {
6
6
  let wrapper;
7
- let component;
8
7
 
9
8
  beforeEach(() => {
10
- wrapper = shallowMount(MultipleFilePicker, {
11
- propsData: {
9
+ wrapper = mount(MultipleFilePicker, {
10
+ props: {
12
11
  maxFileSize: 1,
13
12
  maxFilesNumber: 10,
14
13
  downloadFiles: [],
15
14
  acceptedFileTypes: 'application/pdf,image/jpeg,image/jpg,image/png',
16
15
  },
16
+ global: {
17
+ mocks: {
18
+ 'farm-icon': { template: '<i></i>' },
19
+ 'farm-btn': { template: '<button><slot /></button>' },
20
+ 'farm-subtitle': { template: '<div><slot /></div>' },
21
+ 'farm-bodysmall': { template: '<span><slot /></span>' },
22
+ },
23
+ },
17
24
  });
18
- component = wrapper.vm;
19
25
  });
20
26
 
21
- test('MultipleFilePicker created', () => {
22
- expect(wrapper).toBeDefined();
27
+ test('MultipleFilePicker is created', () => {
28
+ expect(wrapper.exists()).toBe(true);
23
29
  });
24
30
 
25
31
  describe('Methods', () => {
26
- describe('handlerFunctionHighlight', () => {
27
- it('Should add class', () => {
28
- const spyObj = jest.spyOn(wrapper.vm.dropArea.classList, 'add');
29
- wrapper.vm.handlerFunctionHighlight();
30
- expect(spyObj).toHaveBeenCalled();
31
- });
32
- });
33
-
34
- describe('handlerFunctionUnhighlight', () => {
35
- it('Should add class', () => {
36
- const spyObj = jest.spyOn(wrapper.vm.dropArea.classList, 'remove');
37
- wrapper.vm.handlerFunctionUnhighlight();
38
- expect(spyObj).toHaveBeenCalled();
39
- });
40
- });
41
-
42
- describe('addListeners', () => {
43
- it('Should add listeners', () => {
44
- const spyObj = jest.spyOn(wrapper.vm.dropArea, 'addEventListener');
45
- wrapper.vm.addListeners();
46
- expect(spyObj).toHaveBeenCalled();
47
- });
48
- });
32
+ it('handleFileChange should update files and emit event', async () => {
33
+ const file = new File(['file content'], 'test.pdf', { type: 'application/pdf' });
34
+ const input = wrapper.find('input[type="file"]');
49
35
 
50
- describe('fileChange', () => {
51
- it('should open file input', () => {
52
- const file = new File([], 'test');
53
- component.fileChange([file]);
54
- expect(component.files).toEqual([file]);
36
+ Object.defineProperty(input.element, 'files', {
37
+ value: [file],
55
38
  });
39
+
40
+ await input.trigger('change');
41
+ expect(wrapper.vm.files).toHaveLength(1);
42
+ expect(wrapper.emitted().onFileChange).toBeTruthy();
56
43
  });
44
+
57
45
 
58
- describe('reset', () => {
59
- it('should reset', () => {
60
- const file = new File([], 'test');
61
- component.files = [file];
62
- component.reset();
63
- expect(component.files.length).toEqual(0);
64
- });
46
+ it('remove should remove file and emit onFileChange', () => {
47
+ wrapper.vm.files = [{ name: 'test.pdf', size: 1000 }];
48
+ wrapper.vm.remove(0);
49
+ expect(wrapper.vm.files).toHaveLength(0);
50
+ expect(wrapper.emitted().onFileChange).toBeTruthy();
65
51
  });
66
52
 
67
- describe('remove', () => {
68
- it('should remove an item and call reset method when files length is 1', () => {
69
- const spy = jest.spyOn(component, 'reset');
70
- const file = new File([], 'test');
71
- component.files = [file];
72
- component.remove(0);
73
- expect(component.files).toEqual([]);
74
- expect(spy).toHaveBeenCalled();
75
- });
76
-
77
- it('should remove an item', () => {
78
- const spy = jest.spyOn(component, 'reset');
79
- const file = new File([], 'test');
80
- const file2 = new File([], 'test2');
81
- component.files = [file, file2];
82
- component.remove(0);
83
- expect(component.files).toEqual([file2]);
84
- expect(spy).not.toHaveBeenCalled();
85
- });
86
- });
87
- describe('onDownload', () => {
88
- it('should remove an item', () => {
89
- component.onDownload(1);
90
- expect(wrapper.emitted().onDownload[0][0]).toBe(1);
91
- });
53
+ it('addMoreFiles should trigger input click', async () => {
54
+ const input = wrapper.find('input[type="file"]');
55
+ await wrapper.vm.addMoreFiles();
56
+ expect(input.exists()).toBe(true);
92
57
  });
58
+
93
59
  });
94
60
 
95
61
  describe('Computed', () => {
96
- describe('hasFiles', () => {
97
- it('should return false', () => {
98
- expect(component.hasFiles).toBeFalsy();
99
- });
100
-
101
- it('should return true when files length is more than zero', () => {
102
- component.files = [new File([], 'test')];
103
- expect(component.hasFiles).toBeTruthy();
104
- });
105
-
106
- it('should return true when files length is more than zero', async () => {
107
- await wrapper.setProps({
108
- downloadFiles: [new File([], 'test')],
109
- });
110
- expect(component.hasFiles).toBeTruthy();
111
- });
112
- });
113
- describe('filesLength', () => {
114
- it('should return files and downloadFiles length', async () => {
115
- await wrapper.setProps({
116
- downloadFiles: [new File([], 'test')],
117
- });
118
- component.files = [new File([], 'test2')];
119
- expect(component.filesLength).toBe(2);
120
- });
121
- });
122
- describe('disabledButton', () => {
123
- it('should return files and downloadFiles length', async () => {
124
- await wrapper.setProps({
125
- maxFilesNumber: 0,
126
- });
127
-
128
- expect(component.disabledButton).toBeFalsy();
129
- });
130
-
131
- it('should return files and downloadFiles length', async () => {
132
- await wrapper.setProps({
133
- maxFilesNumber: 2,
134
- downloadFiles: [new File([], 'test')],
135
- });
136
- component.files = [new File([], 'test2')];
137
-
138
- expect(component.disabledButton).toBeTruthy();
139
- });
62
+ it('hasFiles should return true if there are files', () => {
63
+ wrapper.vm.files = [{ name: 'test.pdf', size: 1000 }];
64
+ expect(wrapper.vm.hasFiles).toBe(true);
140
65
  });
141
66
 
142
- describe('canAddMoreFiles', () => {
143
- it('should return files and downloadFiles length', async () => {
144
- await wrapper.setProps({
145
- maxFilesNumber: 2,
146
- downloadFiles: [new File([], 'test')],
147
- });
148
- component.files = [new File([], 'test2')];
149
-
150
- expect(component.canAddMoreFiles).toBeFalsy();
151
- });
67
+ it('canAddMoreFiles should return false if maxFilesNumber is reached', () => {
68
+ wrapper.vm.files = Array(10).fill({ name: 'test.pdf', size: 1000 });
69
+ expect(wrapper.vm.canAddMoreFiles).toBe(false);
152
70
  });
153
71
  });
154
72
 
155
- describe('Watch', () => {
156
- describe('files', () => {
157
- it('should emit an onMaxFilesNumberWarning event when there are more files added who maxFilesNumber', () => {
158
- wrapper.vm.$options.watch.files.call(
159
- component,
160
- Array.from(Array(11).keys()).map(() => ({ type: 'image/jpeg' }))
161
- );
162
- expect(wrapper.emitted().onMaxFilesNumberWarning).toBeDefined();
163
- });
164
-
165
- it('should emit an onMaxFileSizeWarning event when one file size is bigger than maxFileSize prop', async () => {
166
- await wrapper.setProps({
167
- maxFileSize: 3,
168
- });
169
- wrapper.vm.$options.watch.files.call(component, [
170
- { size: 4145728, type: 'image/jpeg' },
171
- ]);
172
- expect(wrapper.emitted().onMaxFileSizeWarning).toBeDefined();
173
- });
174
-
175
- it('should not add invalid types files', () => {
176
- wrapper.vm.$options.watch.files.call(
177
- component,
178
- Array.from(Array(1).keys()).map(() => ({ type: 'image/test' }))
179
- );
180
- expect(component.files).toEqual([]);
181
- });
73
+ describe('Watchers', () => {
74
+ it('should emit onMaxFilesNumberWarning if maxFilesNumber is exceeded', async () => {
75
+ wrapper.setProps({ maxFilesNumber: 10 });
76
+
77
+ const files = Array(11).fill(new File(['content'], 'test.pdf', { type: 'application/pdf' }));
78
+ const input = wrapper.find('input[type="file"]');
79
+ Object.defineProperty(input.element, 'files', { value: files });
80
+
81
+ await input.trigger('change');
82
+ expect(wrapper.vm.files).toHaveLength(10);
83
+ expect(wrapper.emitted().onMaxFilesNumberWarning).toBeTruthy();
182
84
  });
85
+
86
+ it('should emit onInvalidFiles if a file exceeds maxFileSize', async () => {
87
+ wrapper.setProps({ maxFileSize: 1 });
88
+
89
+ //cria um arquivo de 3MB
90
+ const file = new File(['a'.repeat(3 * 1024 * 1024)], 'test.pdf', { type: 'application/pdf' });
91
+ const input = wrapper.find('input[type="file"]');
92
+
93
+ Object.defineProperty(input.element, 'files', { value: [file] });
94
+
95
+ await input.trigger('change');
96
+ expect(wrapper.vm.files).toHaveLength(0);
97
+ expect(wrapper.emitted().onInvalidFiles).toBeTruthy();
98
+ });
99
+
183
100
  });
101
+
184
102
  });
@@ -0,0 +1,15 @@
1
+ .collapse-enter-active,
2
+ .collapse-leave-active {
3
+ overflow: hidden;
4
+ transition: max-height 0.5s ease;
5
+ }
6
+
7
+ .collapse-enter-from,
8
+ .collapse-leave-to {
9
+ max-height: 0;
10
+ }
11
+
12
+ .collapse-enter-to,
13
+ .collapse-leave-from {
14
+ max-height: 400px;
15
+ }