@farm-investimentos/front-mfe-components 15.13.2 → 15.14.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.
- package/dist/front-mfe-components.common.js +17718 -17463
- package/dist/front-mfe-components.common.js.map +1 -1
- package/dist/front-mfe-components.css +1 -1
- package/dist/front-mfe-components.umd.js +17718 -17463
- package/dist/front-mfe-components.umd.js.map +1 -1
- package/dist/front-mfe-components.umd.min.js +1 -1
- package/dist/front-mfe-components.umd.min.js.map +1 -1
- package/dist/img/empty-data.a1f5ca32.svg +69 -0
- package/dist/img/empty-not-found.20176d0b.svg +42 -0
- package/package.json +1 -1
- package/src/assets/imgs/empty-data.svg +69 -0
- package/src/assets/imgs/empty-not-found.svg +42 -0
- package/src/components/FilterEmptyState/FilterEmptyState.scss +17 -0
- package/src/components/FilterEmptyState/FilterEmptyState.stories.js +106 -0
- package/src/components/FilterEmptyState/FilterEmptyState.vue +137 -0
- package/src/components/FilterEmptyState/__tests__/FilterEmptyState.test.js +108 -0
- package/src/components/FilterEmptyState/index.ts +38 -0
- package/src/components/GanttChart/GanttChart.scss +5 -0
- package/src/components/GanttChart/GanttChart.vue +2 -2
- package/src/components/GanttChart/__tests__/GanttChart.spec.js +22 -47
- package/src/components/GanttChart/composition/buildBarPositioning.ts +6 -2
- package/src/components/GanttChart/composition/buildGanttData.ts +3 -3
- package/src/components/GanttChart/types/index.ts +0 -3
- package/src/main.ts +5 -0
- package/src/shims-tsx.d.ts +12 -7
- package/src/shims-vue.d.ts +3 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import FilterEmptyState from './FilterEmptyState.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Display/FilterEmptyState',
|
|
5
|
+
component: FilterEmptyState,
|
|
6
|
+
parameters: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: {
|
|
9
|
+
component: `Componente para exibir estados vazios em filtros<br />
|
|
10
|
+
selector: <em>farm-filter-empty-state</em><br />
|
|
11
|
+
<span style="color: var(--farm-primary-base);">ready for use</span>
|
|
12
|
+
`,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
viewMode: 'docs',
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
isEmpty: {
|
|
19
|
+
control: { type: 'boolean' },
|
|
20
|
+
description: 'Indica se o estado é vazio (sem dados)',
|
|
21
|
+
},
|
|
22
|
+
isNotFound: {
|
|
23
|
+
control: { type: 'boolean' },
|
|
24
|
+
description: 'Indica se o estado é não encontrado (sem resultados do filtro)',
|
|
25
|
+
},
|
|
26
|
+
isEmptyImage: {
|
|
27
|
+
control: { type: 'text' },
|
|
28
|
+
description: 'URL da imagem personalizada para estado vazio',
|
|
29
|
+
},
|
|
30
|
+
isEmptyImageAlt: {
|
|
31
|
+
control: { type: 'text' },
|
|
32
|
+
description: 'Texto alternativo da imagem para estado vazio',
|
|
33
|
+
},
|
|
34
|
+
isNotFoundImage: {
|
|
35
|
+
control: { type: 'text' },
|
|
36
|
+
description: 'URL da imagem personalizada para estado não encontrado',
|
|
37
|
+
},
|
|
38
|
+
isNotFoundImageAlt: {
|
|
39
|
+
control: { type: 'text' },
|
|
40
|
+
description: 'Texto alternativo da imagem para estado não encontrado',
|
|
41
|
+
},
|
|
42
|
+
title: {
|
|
43
|
+
control: { type: 'text' },
|
|
44
|
+
description: 'Título personalizado',
|
|
45
|
+
},
|
|
46
|
+
subtitle: {
|
|
47
|
+
control: { type: 'text' },
|
|
48
|
+
description: 'Subtítulo personalizado',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
args: {
|
|
52
|
+
isEmpty: true,
|
|
53
|
+
isNotFound: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Empty state image
|
|
58
|
+
export const EmptyStateImage = args => ({
|
|
59
|
+
components: { FilterEmptyState },
|
|
60
|
+
setup() {
|
|
61
|
+
return { args };
|
|
62
|
+
},
|
|
63
|
+
template: '<farm-filter-empty-state v-bind="args" />',
|
|
64
|
+
});
|
|
65
|
+
EmptyStateImage.storyName = 'Empty state image';
|
|
66
|
+
EmptyStateImage.args = {
|
|
67
|
+
isEmpty: true,
|
|
68
|
+
isNotFound: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Not found state image
|
|
72
|
+
export const NotFoundStateImage = args => ({
|
|
73
|
+
components: { FilterEmptyState },
|
|
74
|
+
setup() {
|
|
75
|
+
return { args };
|
|
76
|
+
},
|
|
77
|
+
template: '<farm-filter-empty-state v-bind="args" />',
|
|
78
|
+
});
|
|
79
|
+
NotFoundStateImage.storyName = 'Not found state image';
|
|
80
|
+
NotFoundStateImage.args = {
|
|
81
|
+
isEmpty: false,
|
|
82
|
+
isNotFound: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Custom titles and subtitles (slot)
|
|
86
|
+
export const CustomTitlesAndSubtitles = args => ({
|
|
87
|
+
components: { FilterEmptyState },
|
|
88
|
+
setup() {
|
|
89
|
+
return { args };
|
|
90
|
+
},
|
|
91
|
+
template: `
|
|
92
|
+
<farm-filter-empty-state v-bind="args">
|
|
93
|
+
<template #title>
|
|
94
|
+
<span style="color: #4f8406;">Título customizado via slot</span>
|
|
95
|
+
</template>
|
|
96
|
+
<template #subtitle>
|
|
97
|
+
<span>Subtítulo <b>com HTML</b> via slot</span>
|
|
98
|
+
</template>
|
|
99
|
+
</farm-filter-empty-state>
|
|
100
|
+
`,
|
|
101
|
+
});
|
|
102
|
+
CustomTitlesAndSubtitles.storyName = 'Custom titles and subtitles (slot)';
|
|
103
|
+
CustomTitlesAndSubtitles.args = {
|
|
104
|
+
isEmpty: true,
|
|
105
|
+
isNotFound: false,
|
|
106
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<farm-box>
|
|
3
|
+
<farm-row justify="center" class="mb-4">
|
|
4
|
+
<img
|
|
5
|
+
v-if="isEmpty"
|
|
6
|
+
:src="isEmptyImage"
|
|
7
|
+
:alt="isEmptyImageAlt"
|
|
8
|
+
class="filter-empty-state__image"
|
|
9
|
+
/>
|
|
10
|
+
<img
|
|
11
|
+
v-else-if="isNotFound"
|
|
12
|
+
:src="isNotFoundImage"
|
|
13
|
+
:alt="isNotFoundImageAlt"
|
|
14
|
+
class="filter-empty-state__image"
|
|
15
|
+
/>
|
|
16
|
+
</farm-row>
|
|
17
|
+
<farm-row justify="center">
|
|
18
|
+
<farm-bodytext variation="bold" color="gray" :type="2">
|
|
19
|
+
<slot name="title">
|
|
20
|
+
{{ title || 'Nenhuma informação para exibir aqui' }}
|
|
21
|
+
</slot>
|
|
22
|
+
</farm-bodytext>
|
|
23
|
+
</farm-row>
|
|
24
|
+
<farm-row justify="center" class="mt-2">
|
|
25
|
+
<farm-caption variation="regular" color="gray">
|
|
26
|
+
<slot name="subtitle">
|
|
27
|
+
{{ subtitle || 'Tente filtrar novamente sua pesquisa.' }}
|
|
28
|
+
</slot>
|
|
29
|
+
</farm-caption>
|
|
30
|
+
</farm-row>
|
|
31
|
+
</farm-box>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { defineComponent, PropType } from 'vue';
|
|
36
|
+
|
|
37
|
+
export type FilterEmptyStateProps = {
|
|
38
|
+
/**
|
|
39
|
+
* Indicates if the state is empty (no data)
|
|
40
|
+
*/
|
|
41
|
+
isEmpty?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Indicates if the state is not found (no results from filter)
|
|
44
|
+
*/
|
|
45
|
+
isNotFound?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Custom image URL for empty state
|
|
48
|
+
*/
|
|
49
|
+
isEmptyImage?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Alt text for empty state image
|
|
52
|
+
*/
|
|
53
|
+
isEmptyImageAlt?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Custom image URL for not found state
|
|
56
|
+
*/
|
|
57
|
+
isNotFoundImage?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Alt text for not found state image
|
|
60
|
+
*/
|
|
61
|
+
isNotFoundImageAlt?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Title to be displayed
|
|
64
|
+
*/
|
|
65
|
+
title?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Subtitle to be displayed
|
|
68
|
+
*/
|
|
69
|
+
subtitle?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default defineComponent({
|
|
73
|
+
name: 'farm-filter-empty-state',
|
|
74
|
+
props: {
|
|
75
|
+
/**
|
|
76
|
+
* Indicates if the state is empty (no data)
|
|
77
|
+
*/
|
|
78
|
+
isEmpty: {
|
|
79
|
+
type: Boolean as PropType<FilterEmptyStateProps['isEmpty']>,
|
|
80
|
+
default: false,
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Indicates if the state is not found (no results from filter)
|
|
84
|
+
*/
|
|
85
|
+
isNotFound: {
|
|
86
|
+
type: Boolean as PropType<FilterEmptyStateProps['isNotFound']>,
|
|
87
|
+
default: false,
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* Custom image URL for empty state
|
|
91
|
+
*/
|
|
92
|
+
isEmptyImage: {
|
|
93
|
+
type: String as PropType<FilterEmptyStateProps['isEmptyImage']>,
|
|
94
|
+
default: require('../../assets/imgs/empty-data.svg'),
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Alt text for empty state image
|
|
98
|
+
*/
|
|
99
|
+
isEmptyImageAlt: {
|
|
100
|
+
type: String as PropType<FilterEmptyStateProps['isEmptyImageAlt']>,
|
|
101
|
+
default: 'Imagem referente a dados vazios',
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Custom image URL for not found state
|
|
105
|
+
*/
|
|
106
|
+
isNotFoundImage: {
|
|
107
|
+
type: String as PropType<FilterEmptyStateProps['isNotFoundImage']>,
|
|
108
|
+
default: require('../../assets/imgs/empty-not-found.svg'),
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Alt text for not found state image
|
|
112
|
+
*/
|
|
113
|
+
isNotFoundImageAlt: {
|
|
114
|
+
type: String as PropType<FilterEmptyStateProps['isNotFoundImageAlt']>,
|
|
115
|
+
default: 'Imagem referente a filtro vazio',
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Title to be displayed
|
|
119
|
+
*/
|
|
120
|
+
title: {
|
|
121
|
+
type: String as PropType<FilterEmptyStateProps['title']>,
|
|
122
|
+
default: '',
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Subtitle to be displayed
|
|
126
|
+
*/
|
|
127
|
+
subtitle: {
|
|
128
|
+
type: String as PropType<FilterEmptyStateProps['subtitle']>,
|
|
129
|
+
default: '',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<style scoped lang="scss">
|
|
136
|
+
@import './FilterEmptyState.scss';
|
|
137
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import FilterEmptyState from '../FilterEmptyState.vue';
|
|
3
|
+
|
|
4
|
+
const globalStubs = {
|
|
5
|
+
'farm-box': { template: '<div><slot /></div>' },
|
|
6
|
+
'farm-row': { template: '<div><slot /></div>' },
|
|
7
|
+
'farm-bodytext': { template: '<div><slot /></div>' },
|
|
8
|
+
'farm-caption': { template: '<div><slot /></div>' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('FilterEmptyState', () => {
|
|
12
|
+
it('renders empty state correctly', () => {
|
|
13
|
+
const wrapper = mount(FilterEmptyState, {
|
|
14
|
+
propsData: {
|
|
15
|
+
isEmpty: true,
|
|
16
|
+
isNotFound: false,
|
|
17
|
+
},
|
|
18
|
+
stubs: globalStubs,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(wrapper.find('.filter-empty-state__image').exists()).toBe(true);
|
|
22
|
+
expect(wrapper.text()).toContain('Nenhuma informação para exibir aqui');
|
|
23
|
+
expect(wrapper.text()).toContain('Tente filtrar novamente sua pesquisa.');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders not found state correctly', () => {
|
|
27
|
+
const wrapper = mount(FilterEmptyState, {
|
|
28
|
+
propsData: {
|
|
29
|
+
isEmpty: false,
|
|
30
|
+
isNotFound: true,
|
|
31
|
+
},
|
|
32
|
+
stubs: globalStubs,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(wrapper.find('.filter-empty-state__image').exists()).toBe(true);
|
|
36
|
+
expect(wrapper.text()).toContain('Nenhuma informação para exibir aqui');
|
|
37
|
+
expect(wrapper.text()).toContain('Tente filtrar novamente sua pesquisa.');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders custom title and subtitle', () => {
|
|
41
|
+
const wrapper = mount(FilterEmptyState, {
|
|
42
|
+
propsData: {
|
|
43
|
+
isEmpty: true,
|
|
44
|
+
title: 'Custom Title',
|
|
45
|
+
subtitle: 'Custom Subtitle',
|
|
46
|
+
},
|
|
47
|
+
stubs: globalStubs,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(wrapper.text()).toContain('Custom Title');
|
|
51
|
+
expect(wrapper.text()).toContain('Custom Subtitle');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses custom images when provided', () => {
|
|
55
|
+
const wrapper = mount(FilterEmptyState, {
|
|
56
|
+
propsData: {
|
|
57
|
+
isEmpty: true,
|
|
58
|
+
isEmptyImage: 'custom-empty-image.svg',
|
|
59
|
+
isEmptyImageAlt: 'Custom empty image',
|
|
60
|
+
},
|
|
61
|
+
stubs: globalStubs,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const img = wrapper.find('.filter-empty-state__image');
|
|
65
|
+
expect(img.exists()).toBe(true);
|
|
66
|
+
expect(img.attributes('src')).toBe('custom-empty-image.svg');
|
|
67
|
+
expect(img.attributes('alt')).toBe('Custom empty image');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('uses custom not found images when provided', () => {
|
|
71
|
+
const wrapper = mount(FilterEmptyState, {
|
|
72
|
+
propsData: {
|
|
73
|
+
isNotFound: true,
|
|
74
|
+
isNotFoundImage: 'custom-not-found-image.svg',
|
|
75
|
+
isNotFoundImageAlt: 'Custom not found image',
|
|
76
|
+
},
|
|
77
|
+
stubs: globalStubs,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const img = wrapper.find('.filter-empty-state__image');
|
|
81
|
+
expect(img.exists()).toBe(true);
|
|
82
|
+
expect(img.attributes('src')).toBe('custom-not-found-image.svg');
|
|
83
|
+
expect(img.attributes('alt')).toBe('Custom not found image');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('has correct default props', () => {
|
|
87
|
+
const wrapper = mount(FilterEmptyState, {
|
|
88
|
+
stubs: globalStubs,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(wrapper.props('isEmpty')).toBe(false);
|
|
92
|
+
expect(wrapper.props('isNotFound')).toBe(false);
|
|
93
|
+
expect(wrapper.props('title')).toBe('');
|
|
94
|
+
expect(wrapper.props('subtitle')).toBe('');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not show image when neither isEmpty nor isNotFound is true', () => {
|
|
98
|
+
const wrapper = mount(FilterEmptyState, {
|
|
99
|
+
propsData: {
|
|
100
|
+
isEmpty: false,
|
|
101
|
+
isNotFound: false,
|
|
102
|
+
},
|
|
103
|
+
stubs: globalStubs,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(wrapper.find('.filter-empty-state__image').exists()).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import FilterEmptyState from './FilterEmptyState.vue';
|
|
2
|
+
|
|
3
|
+
export type FilterEmptyStateProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Indicates if the state is empty (no data)
|
|
6
|
+
*/
|
|
7
|
+
isEmpty?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Indicates if the state is not found (no results from filter)
|
|
10
|
+
*/
|
|
11
|
+
isNotFound?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Custom image URL for empty state
|
|
14
|
+
*/
|
|
15
|
+
isEmptyImage?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Alt text for empty state image
|
|
18
|
+
*/
|
|
19
|
+
isEmptyImageAlt?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Custom image URL for not found state
|
|
22
|
+
*/
|
|
23
|
+
isNotFoundImage?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Alt text for not found state image
|
|
26
|
+
*/
|
|
27
|
+
isNotFoundImageAlt?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Title to be displayed
|
|
30
|
+
*/
|
|
31
|
+
title?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Subtitle to be displayed
|
|
34
|
+
*/
|
|
35
|
+
subtitle?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default FilterEmptyState;
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
margin-top: gutter('lg');
|
|
137
137
|
padding-top: gutter('sm');
|
|
138
138
|
flex-wrap: wrap;
|
|
139
|
+
width: 100%;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
&__legend-title {
|
|
@@ -213,6 +214,10 @@
|
|
|
213
214
|
margin-bottom: 0;
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
.tooltip-label {
|
|
218
|
+
margin-right: 4px;
|
|
219
|
+
}
|
|
220
|
+
|
|
216
221
|
|
|
217
222
|
.tooltip-text {
|
|
218
223
|
font-weight: 500;
|
|
@@ -227,9 +227,9 @@ export default defineComponent({
|
|
|
227
227
|
|
|
228
228
|
const legendStyle = computed(() => {
|
|
229
229
|
const timelineWidth = monthColumns.value.length * 80;
|
|
230
|
-
const totalWidth =
|
|
230
|
+
const totalWidth = 180 + timelineWidth;
|
|
231
231
|
return {
|
|
232
|
-
|
|
232
|
+
minWidth: `${totalWidth}px`,
|
|
233
233
|
};
|
|
234
234
|
});
|
|
235
235
|
|
|
@@ -93,15 +93,15 @@ describe('GanttChart component', () => {
|
|
|
93
93
|
{
|
|
94
94
|
id: 1,
|
|
95
95
|
label: 'Early Bar',
|
|
96
|
-
start: new Date(2025, 0, 15),
|
|
97
|
-
end: new Date(2025, 2, 10),
|
|
96
|
+
start: new Date(2025, 0, 15),
|
|
97
|
+
end: new Date(2025, 2, 10),
|
|
98
98
|
color: '#7BC4F7',
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
id: 2,
|
|
102
102
|
label: 'Late Bar',
|
|
103
|
-
start: new Date(2025, 4, 5),
|
|
104
|
-
end: new Date(2025, 6, 20),
|
|
103
|
+
start: new Date(2025, 4, 5),
|
|
104
|
+
end: new Date(2025, 6, 20),
|
|
105
105
|
color: '#8BB455',
|
|
106
106
|
},
|
|
107
107
|
],
|
|
@@ -113,11 +113,10 @@ describe('GanttChart component', () => {
|
|
|
113
113
|
propsData: { data: testData },
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
// Should calculate from January 1st to July 31st (full months)
|
|
117
116
|
const monthColumns = testWrapper.vm.monthColumns;
|
|
118
|
-
expect(monthColumns.length).toBe(
|
|
117
|
+
expect(monthColumns.length).toBe(8);
|
|
119
118
|
expect(monthColumns[0].label).toContain('Jan');
|
|
120
|
-
expect(monthColumns[monthColumns.length - 1].label).toContain('
|
|
119
|
+
expect(monthColumns[monthColumns.length - 1].label).toContain('Ago');
|
|
121
120
|
});
|
|
122
121
|
|
|
123
122
|
it('should handle single month range', () => {
|
|
@@ -129,8 +128,8 @@ describe('GanttChart component', () => {
|
|
|
129
128
|
{
|
|
130
129
|
id: 1,
|
|
131
130
|
label: 'Single Month Bar',
|
|
132
|
-
start: new Date(2025, 3, 5),
|
|
133
|
-
end: new Date(2025, 3, 25),
|
|
131
|
+
start: new Date(2025, 3, 5),
|
|
132
|
+
end: new Date(2025, 3, 25),
|
|
134
133
|
color: '#7BC4F7',
|
|
135
134
|
},
|
|
136
135
|
],
|
|
@@ -143,8 +142,9 @@ describe('GanttChart component', () => {
|
|
|
143
142
|
});
|
|
144
143
|
|
|
145
144
|
const monthColumns = testWrapper.vm.monthColumns;
|
|
146
|
-
expect(monthColumns.length).toBe(
|
|
145
|
+
expect(monthColumns.length).toBe(2);
|
|
147
146
|
expect(monthColumns[0].label).toContain('Abr');
|
|
147
|
+
expect(monthColumns[1].label).toContain('Mai');
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
150
|
|
|
@@ -171,7 +171,7 @@ describe('GanttChart component', () => {
|
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
id: 3,
|
|
174
|
-
label: 'Design',
|
|
174
|
+
label: 'Design',
|
|
175
175
|
start: new Date(2025, 2, 1),
|
|
176
176
|
end: new Date(2025, 3, 1),
|
|
177
177
|
color: '#8E44AD',
|
|
@@ -186,7 +186,7 @@ describe('GanttChart component', () => {
|
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
const legend = testWrapper.vm.autoGeneratedLegend;
|
|
189
|
-
expect(legend).toHaveLength(2);
|
|
189
|
+
expect(legend).toHaveLength(2);
|
|
190
190
|
expect(legend.some(item => item.label === 'Design' && item.color === '#8E44AD')).toBe(true);
|
|
191
191
|
expect(legend.some(item => item.label === 'Development' && item.color === '#16A085')).toBe(true);
|
|
192
192
|
});
|
|
@@ -219,7 +219,6 @@ describe('GanttChart component', () => {
|
|
|
219
219
|
];
|
|
220
220
|
const positionedBars = component.getPositionedBars(bars);
|
|
221
221
|
expect(positionedBars).toHaveLength(2);
|
|
222
|
-
// Each bar should have its own unique row position
|
|
223
222
|
expect(positionedBars[0].rowPosition).toBe(0);
|
|
224
223
|
expect(positionedBars[1].rowPosition).toBe(1);
|
|
225
224
|
});
|
|
@@ -228,21 +227,21 @@ describe('GanttChart component', () => {
|
|
|
228
227
|
const bars = [
|
|
229
228
|
{
|
|
230
229
|
id: 3,
|
|
231
|
-
start: new Date(2025, 2, 1),
|
|
230
|
+
start: new Date(2025, 2, 1),
|
|
232
231
|
end: new Date(2025, 2, 15),
|
|
233
232
|
label: 'Latest Bar',
|
|
234
233
|
color: '#FFB84D',
|
|
235
234
|
},
|
|
236
235
|
{
|
|
237
236
|
id: 1,
|
|
238
|
-
start: new Date(2025, 0, 1),
|
|
237
|
+
start: new Date(2025, 0, 1),
|
|
239
238
|
end: new Date(2025, 0, 15),
|
|
240
239
|
label: 'Early Bar',
|
|
241
240
|
color: '#7BC4F7',
|
|
242
241
|
},
|
|
243
242
|
{
|
|
244
243
|
id: 2,
|
|
245
|
-
start: new Date(2025, 1, 1),
|
|
244
|
+
start: new Date(2025, 1, 1),
|
|
246
245
|
end: new Date(2025, 1, 15),
|
|
247
246
|
label: 'Middle Bar',
|
|
248
247
|
color: '#8BB455',
|
|
@@ -250,10 +249,9 @@ describe('GanttChart component', () => {
|
|
|
250
249
|
];
|
|
251
250
|
const positionedBars = component.getPositionedBars(bars);
|
|
252
251
|
expect(positionedBars).toHaveLength(3);
|
|
253
|
-
|
|
254
|
-
expect(positionedBars[
|
|
255
|
-
expect(positionedBars[
|
|
256
|
-
expect(positionedBars[2].id).toBe(2); // Middle date stays third
|
|
252
|
+
expect(positionedBars[0].id).toBe(3);
|
|
253
|
+
expect(positionedBars[1].id).toBe(1);
|
|
254
|
+
expect(positionedBars[2].id).toBe(2);
|
|
257
255
|
expect(positionedBars[0].rowPosition).toBe(0);
|
|
258
256
|
expect(positionedBars[1].rowPosition).toBe(1);
|
|
259
257
|
expect(positionedBars[2].rowPosition).toBe(2);
|
|
@@ -310,7 +308,6 @@ describe('GanttChart component', () => {
|
|
|
310
308
|
label: 'Bar Without Color',
|
|
311
309
|
start: new Date(2025, 0, 1),
|
|
312
310
|
end: new Date(2025, 1, 1),
|
|
313
|
-
// color não definido - deve usar fallback
|
|
314
311
|
}]
|
|
315
312
|
}]
|
|
316
313
|
};
|
|
@@ -350,16 +347,13 @@ describe('GanttChart component', () => {
|
|
|
350
347
|
it('should validate invalid data prop structure', () => {
|
|
351
348
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
352
349
|
|
|
353
|
-
// Teste com data null
|
|
354
350
|
const validator = wrapper.vm.$options.props.data.validator;
|
|
355
351
|
expect(validator(null)).toBe(false);
|
|
356
352
|
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data" deve ser um objeto.');
|
|
357
353
|
|
|
358
|
-
// Teste com data sem groups
|
|
359
354
|
expect(validator({})).toBe(false);
|
|
360
355
|
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
|
|
361
356
|
|
|
362
|
-
// Teste com groups inválido
|
|
363
357
|
expect(validator({ groups: 'invalid' })).toBe(false);
|
|
364
358
|
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
|
|
365
359
|
|
|
@@ -370,7 +364,6 @@ describe('GanttChart component', () => {
|
|
|
370
364
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
371
365
|
const validator = wrapper.vm.$options.props.data.validator;
|
|
372
366
|
|
|
373
|
-
// Teste com grupo sem title
|
|
374
367
|
const invalidData = {
|
|
375
368
|
groups: [{
|
|
376
369
|
bars: []
|
|
@@ -379,7 +372,6 @@ describe('GanttChart component', () => {
|
|
|
379
372
|
expect(validator(invalidData)).toBe(false);
|
|
380
373
|
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: cada grupo deve ter título (string) e barras (array).');
|
|
381
374
|
|
|
382
|
-
// Teste com grupo sem bars
|
|
383
375
|
const invalidData2 = {
|
|
384
376
|
groups: [{
|
|
385
377
|
title: 'Valid Title'
|
|
@@ -408,18 +400,15 @@ describe('GanttChart component', () => {
|
|
|
408
400
|
const bar = {
|
|
409
401
|
id: 1,
|
|
410
402
|
label: 'Inverted Date Bar',
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
end: new Date(2025, 2, 15), // 15 de março (antes do início)
|
|
403
|
+
start: new Date(2025, 3, 15),
|
|
404
|
+
end: new Date(2025, 2, 15),
|
|
414
405
|
color: '#7BC4F7',
|
|
415
406
|
};
|
|
416
407
|
|
|
417
|
-
// Normalizar barra via função de posicionamento
|
|
418
408
|
const dates = component.normalizeBarDates(bar);
|
|
419
409
|
|
|
420
|
-
|
|
421
|
-
expect(dates.
|
|
422
|
-
expect(dates.endDate.getTime()).toBe(new Date(2025, 3, 15).getTime()); // Agora é 15 de abril
|
|
410
|
+
expect(dates.startDate.getTime()).toBe(new Date(2025, 2, 15).getTime());
|
|
411
|
+
expect(dates.endDate.getTime()).toBe(new Date(2025, 3, 15).getTime());
|
|
423
412
|
});
|
|
424
413
|
|
|
425
414
|
it('should handle invalid dates', () => {
|
|
@@ -438,17 +427,14 @@ describe('GanttChart component', () => {
|
|
|
438
427
|
|
|
439
428
|
describe('Tooltip System', () => {
|
|
440
429
|
it('should show tooltip on bar mouseenter', async () => {
|
|
441
|
-
// Encontrar uma barra
|
|
442
430
|
const bar = wrapper.find('.farm-gantt-chart__bar');
|
|
443
431
|
|
|
444
432
|
if (bar.exists()) {
|
|
445
|
-
// Disparar evento mouseenter
|
|
446
433
|
await bar.trigger('mouseenter', {
|
|
447
434
|
clientX: 100,
|
|
448
435
|
clientY: 100,
|
|
449
436
|
});
|
|
450
437
|
|
|
451
|
-
// Verificar se tooltipState foi atualizado
|
|
452
438
|
expect(component.tooltipState.visible).toBe(true);
|
|
453
439
|
expect(component.tooltipState.title).toBe('Test Bar');
|
|
454
440
|
expect(component.tooltipState.barData).not.toBeNull();
|
|
@@ -456,24 +442,20 @@ describe('GanttChart component', () => {
|
|
|
456
442
|
});
|
|
457
443
|
|
|
458
444
|
it('should hide tooltip on bar mouseleave', async () => {
|
|
459
|
-
// Preparar estado (mostrar tooltip)
|
|
460
445
|
component.tooltipState.visible = true;
|
|
461
446
|
component.tooltipState.title = 'Teste';
|
|
462
447
|
component.tooltipState.barData = { label: 'Teste' };
|
|
463
448
|
|
|
464
|
-
// Encontrar e disparar mouseleave
|
|
465
449
|
const bar = wrapper.find('.farm-gantt-chart__bar');
|
|
466
450
|
if (bar.exists()) {
|
|
467
451
|
await bar.trigger('mouseleave');
|
|
468
452
|
|
|
469
|
-
// Verificar se tooltipState foi atualizado
|
|
470
453
|
expect(component.tooltipState.visible).toBe(false);
|
|
471
454
|
expect(component.tooltipState.barData).toBeNull();
|
|
472
455
|
}
|
|
473
456
|
});
|
|
474
457
|
|
|
475
458
|
it('should show structured tooltip when tooltipData is available', () => {
|
|
476
|
-
// Criar wrapper com dados para tooltip
|
|
477
459
|
const testData = {
|
|
478
460
|
groups: [{
|
|
479
461
|
title: 'Group with Tooltips',
|
|
@@ -495,7 +477,6 @@ describe('GanttChart component', () => {
|
|
|
495
477
|
propsData: { data: testData },
|
|
496
478
|
});
|
|
497
479
|
|
|
498
|
-
// Simular mouseenter
|
|
499
480
|
const barWithTooltip = tooltipWrapper.find('.farm-gantt-chart__bar');
|
|
500
481
|
if (barWithTooltip.exists()) {
|
|
501
482
|
barWithTooltip.trigger('mouseenter', {
|
|
@@ -503,8 +484,6 @@ describe('GanttChart component', () => {
|
|
|
503
484
|
clientY: 100
|
|
504
485
|
});
|
|
505
486
|
|
|
506
|
-
// Verificar que tooltip com dados estruturados será exibido
|
|
507
|
-
// quando tooltipState.barData.tooltipData existir
|
|
508
487
|
expect(tooltipWrapper.vm.tooltipState.barData.tooltipData).toBeDefined();
|
|
509
488
|
expect(tooltipWrapper.vm.tooltipState.barData.tooltipData.Taxa).toBe('1,75%');
|
|
510
489
|
}
|
|
@@ -518,11 +497,8 @@ describe('GanttChart component', () => {
|
|
|
518
497
|
propsData: { data: emptyData },
|
|
519
498
|
});
|
|
520
499
|
|
|
521
|
-
// Componente deve renderizar sem erros mesmo com dados vazios
|
|
522
|
-
expect(emptyWrapper.exists()).toBe(true);
|
|
523
500
|
expect(emptyWrapper.find('.farm-gantt-chart').exists()).toBe(true);
|
|
524
501
|
|
|
525
|
-
// Não deve haver grupos renderizados
|
|
526
502
|
const groups = emptyWrapper.findAll('.farm-gantt-chart__group');
|
|
527
503
|
expect(groups.length).toBe(0);
|
|
528
504
|
});
|
|
@@ -533,7 +509,6 @@ describe('GanttChart component', () => {
|
|
|
533
509
|
propsData: { data: emptyData },
|
|
534
510
|
});
|
|
535
511
|
|
|
536
|
-
// Timeline headers devem estar presentes (baseado em data range padrão ou calculado)
|
|
537
512
|
const header = emptyWrapper.find('.farm-gantt-chart__header');
|
|
538
513
|
expect(header.exists()).toBe(true);
|
|
539
514
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { isValid } from 'date-fns';
|
|
2
|
-
import
|
|
2
|
+
import { Ref } from 'vue';
|
|
3
|
+
import type { GanttBar, DateRange, MonthColumn } from '../types';
|
|
3
4
|
import { getColumnForDate, getDaysInMonth } from '../utils/dateUtils';
|
|
4
5
|
|
|
5
|
-
export default function buildBarPositioning(
|
|
6
|
+
export default function buildBarPositioning(
|
|
7
|
+
dateRange: Ref<DateRange>,
|
|
8
|
+
monthColumns: Ref<MonthColumn[]>
|
|
9
|
+
) {
|
|
6
10
|
const normalizeBarDates = (bar: GanttBar) => {
|
|
7
11
|
let startDate = bar.start instanceof Date ? bar.start : new Date(bar.start);
|
|
8
12
|
let endDate = bar.end instanceof Date ? bar.end : new Date(bar.end);
|