@rancher/shell 3.0.9-rc.6 → 3.0.10
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/assets/styles/base/_color.scss +4 -0
- package/assets/styles/themes/_light.scss +6 -6
- package/assets/styles/themes/_modern.scss +14 -6
- package/assets/translations/en-us.yaml +2 -5
- package/components/CopyToClipboard.vue +28 -0
- package/components/CopyToClipboardText.vue +4 -0
- package/components/CruResource.vue +1 -0
- package/components/GlobalRoleBindings.vue +1 -5
- package/components/IconOrSvg.vue +61 -42
- package/components/ResourceDetail/index.vue +0 -21
- package/components/SortableTable/index.vue +2 -2
- package/components/__tests__/CruResource.test.ts +35 -1
- package/components/form/BannerSettings.vue +2 -2
- package/components/form/NotificationSettings.vue +2 -2
- package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
- package/composables/useIsNewDetailPageEnabled.ts +12 -0
- package/config/product/explorer.js +11 -1
- package/config/product/manager.js +0 -1
- package/config/table-headers.js +0 -9
- package/config/types.js +0 -1
- package/detail/fleet.cattle.io.cluster.vue +1 -1
- package/dialog/FeatureFlagListDialog.vue +1 -1
- package/edit/auth/github-app-steps.vue +2 -0
- package/edit/auth/github-steps.vue +2 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +1 -1
- package/edit/management.cattle.io.user.vue +60 -35
- package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/auth.spec.ts +145 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/index.test.ts +202 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/tls.spec.ts +226 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +24 -21
- package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/opsgenie.spec.ts +157 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/pagerduty.spec.ts +132 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/slack.spec.ts +108 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +2 -1
- package/edit/monitoring.coreos.com.receiver/__tests__/auth.spec.ts +165 -0
- package/edit/monitoring.coreos.com.receiver/__tests__/index.test.ts +153 -0
- package/edit/monitoring.coreos.com.receiver/__tests__/tls.spec.ts +115 -0
- package/edit/monitoring.coreos.com.receiver/types/__tests__/email.spec.ts +86 -0
- package/edit/monitoring.coreos.com.receiver/types/__tests__/opsgenie.spec.ts +209 -0
- package/edit/monitoring.coreos.com.receiver/types/__tests__/pagerduty.spec.ts +105 -0
- package/edit/monitoring.coreos.com.receiver/types/__tests__/slack.spec.ts +92 -0
- package/edit/monitoring.coreos.com.receiver/types/__tests__/webhook.spec.ts +131 -0
- package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +14 -12
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -5
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +18 -3
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +100 -76
- package/edit/token.vue +29 -68
- package/list/provisioning.cattle.io.cluster.vue +2 -2
- package/models/__tests__/chart.test.ts +2 -2
- package/models/chart.js +3 -3
- package/models/token.js +0 -4
- package/package.json +8 -8
- package/pages/account/index.vue +67 -96
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +108 -24
- package/pages/c/_cluster/apps/charts/index.vue +1 -11
- package/pages/c/_cluster/explorer/index.vue +2 -19
- package/pages/c/_cluster/explorer/tools/index.vue +1 -1
- package/pages/c/_cluster/manager/cloudCredential/index.vue +1 -1
- package/pages/c/_cluster/uiplugins/index.vue +1 -1
- package/pkg/auto-import.js +41 -0
- package/plugins/dashboard-store/resource-class.js +2 -2
- package/plugins/steve/__tests__/steve-class.test.ts +1 -1
- package/plugins/steve/steve-class.js +3 -3
- package/plugins/steve/steve-pagination-utils.ts +2 -4
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
- package/rancher-components/RcIcon/types.ts +2 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +8 -1
- package/rancher-components/RcSection/RcSection.test.ts +323 -0
- package/rancher-components/RcSection/RcSection.vue +252 -0
- package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
- package/rancher-components/RcSection/RcSectionActions.vue +85 -0
- package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
- package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
- package/rancher-components/RcSection/index.ts +12 -0
- package/rancher-components/RcSection/types.ts +86 -0
- package/scripts/test-plugins-build.sh +5 -4
- package/types/shell/index.d.ts +93 -108
- package/utils/style.ts +17 -0
- package/utils/svg-filter.js +4 -3
- package/utils/units.js +14 -5
- package/models/ext.cattle.io.token.js +0 -48
|
@@ -11,27 +11,27 @@ const displayCount = computed(() => props.count < 1000 ? props.count : '999+');
|
|
|
11
11
|
:class="{[props.type]: true, disabled: props.disabled}"
|
|
12
12
|
data-testid="rc-counter-badge"
|
|
13
13
|
>
|
|
14
|
-
{{ displayCount }}
|
|
14
|
+
<span class="count">{{ displayCount }}</span>
|
|
15
15
|
</div>
|
|
16
16
|
</template>
|
|
17
17
|
|
|
18
18
|
<style lang="scss" scoped>
|
|
19
19
|
.rc-counter-badge {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
height: 21px;
|
|
22
|
+
|
|
20
23
|
display: inline-flex;
|
|
21
|
-
padding:
|
|
24
|
+
padding: 2px 8px;
|
|
22
25
|
align-items: center;
|
|
23
|
-
gap: 8px;
|
|
24
26
|
|
|
25
27
|
border-radius: 30px;
|
|
26
28
|
border: 1px solid var(--rc-active-border);
|
|
27
29
|
|
|
28
|
-
overflow: hidden;
|
|
29
|
-
text-overflow: ellipsis;
|
|
30
30
|
font-family: Lato;
|
|
31
|
-
font-size:
|
|
31
|
+
font-size: 12px;
|
|
32
32
|
font-style: normal;
|
|
33
33
|
font-weight: 400;
|
|
34
|
-
line-height:
|
|
34
|
+
line-height: 17px;
|
|
35
35
|
color: var(--body-text);
|
|
36
36
|
|
|
37
37
|
&.active {
|
|
@@ -20,17 +20,20 @@ const { backgroundColor, borderColor, textColor } = useStatusColors(status, 'out
|
|
|
20
20
|
|
|
21
21
|
<style lang="scss" scoped>
|
|
22
22
|
.rc-status-badge {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
height: 21px;
|
|
25
|
+
|
|
23
26
|
display: inline-flex;
|
|
24
27
|
align-items: center;
|
|
25
28
|
justify-content: center;
|
|
26
|
-
padding:
|
|
29
|
+
padding: 2px 7px;
|
|
27
30
|
|
|
28
31
|
border: 1px solid transparent;
|
|
29
32
|
border-radius: 30px;
|
|
30
33
|
|
|
31
34
|
font-family: Lato;
|
|
32
35
|
font-size: 12px;
|
|
33
|
-
line-height:
|
|
36
|
+
line-height: 17px;
|
|
34
37
|
|
|
35
38
|
background-color: v-bind(backgroundColor);
|
|
36
39
|
border-color: v-bind(borderColor);
|
|
@@ -314,7 +314,10 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
|
|
|
314
314
|
</div>
|
|
315
315
|
</div>
|
|
316
316
|
|
|
317
|
-
<slot name="item-card-sub-header"
|
|
317
|
+
<slot name="item-card-sub-header">
|
|
318
|
+
<!-- DIV added to add the gap if the sub-header is not provided -->
|
|
319
|
+
<div />
|
|
320
|
+
</slot>
|
|
318
321
|
|
|
319
322
|
<template v-if="$slots['item-card-content']">
|
|
320
323
|
<slot name="item-card-content">
|
|
@@ -407,6 +410,10 @@ $image-medium-box-width: 48px;
|
|
|
407
410
|
height: 24px;
|
|
408
411
|
color: var(--body-text);
|
|
409
412
|
|
|
413
|
+
&.small {
|
|
414
|
+
height: 32px;
|
|
415
|
+
}
|
|
416
|
+
|
|
410
417
|
&-left,
|
|
411
418
|
&-right {
|
|
412
419
|
display: flex;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import RcSection from './RcSection.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: RcSection', () => {
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
type: 'primary' as const,
|
|
7
|
+
mode: 'with-header' as const,
|
|
8
|
+
background: 'primary' as const,
|
|
9
|
+
expandable: false,
|
|
10
|
+
title: 'Test title',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('type prop', () => {
|
|
14
|
+
it('should apply type-primary class when type is "primary"', () => {
|
|
15
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'primary' } });
|
|
16
|
+
|
|
17
|
+
expect(wrapper.find('.rc-section').classes()).toContain('type-primary');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should apply type-secondary class when type is "secondary"', () => {
|
|
21
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'secondary' } });
|
|
22
|
+
|
|
23
|
+
expect(wrapper.find('.rc-section').classes()).toContain('type-secondary');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('background prop', () => {
|
|
28
|
+
it('should apply bg-primary class when background is "primary"', () => {
|
|
29
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'primary' } });
|
|
30
|
+
|
|
31
|
+
expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should apply bg-secondary class when background is "secondary"', () => {
|
|
35
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'secondary' } });
|
|
36
|
+
|
|
37
|
+
expect(wrapper.find('.rc-section').classes()).toContain('bg-secondary');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should default to "primary" background when no background prop and no parent', () => {
|
|
41
|
+
const { background: _, ...propsWithoutBg } = defaultProps;
|
|
42
|
+
const wrapper = mount(RcSection, { props: propsWithoutBg });
|
|
43
|
+
|
|
44
|
+
expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should alternate background from parent via provide/inject', () => {
|
|
48
|
+
const wrapper = mount(RcSection, {
|
|
49
|
+
props: {
|
|
50
|
+
...defaultProps, background: 'primary', expanded: true
|
|
51
|
+
},
|
|
52
|
+
slots: {
|
|
53
|
+
default: {
|
|
54
|
+
components: { RcSection },
|
|
55
|
+
template: '<RcSection type="secondary" mode="with-header" :expandable="false" title="Child" />',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const childSection = wrapper.findAll('.rc-section')[1];
|
|
61
|
+
|
|
62
|
+
expect(childSection.classes()).toContain('bg-secondary');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should allow explicit background to override the injected alternation', () => {
|
|
66
|
+
const wrapper = mount(RcSection, {
|
|
67
|
+
props: {
|
|
68
|
+
...defaultProps, background: 'primary', expanded: true
|
|
69
|
+
},
|
|
70
|
+
slots: {
|
|
71
|
+
default: {
|
|
72
|
+
components: { RcSection },
|
|
73
|
+
template: '<RcSection type="secondary" mode="with-header" :expandable="false" background="primary" title="Child" />',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const childSection = wrapper.findAll('.rc-section')[1];
|
|
79
|
+
|
|
80
|
+
expect(childSection.classes()).toContain('bg-primary');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('mode prop', () => {
|
|
85
|
+
it('should render section-header when mode is "with-header"', () => {
|
|
86
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'with-header' } });
|
|
87
|
+
|
|
88
|
+
expect(wrapper.find('.section-header').exists()).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not render section-header when mode is "no-header"', () => {
|
|
92
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
|
|
93
|
+
|
|
94
|
+
expect(wrapper.find('.section-header').exists()).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should apply no-header class to content when mode is "no-header"', () => {
|
|
98
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
|
|
99
|
+
|
|
100
|
+
expect(wrapper.find('.section-content').classes()).toContain('no-header');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('title prop', () => {
|
|
105
|
+
it('should render the title text', () => {
|
|
106
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, title: 'My Section' } });
|
|
107
|
+
|
|
108
|
+
expect(wrapper.find('.title').text()).toBe('My Section');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should render the title slot when provided', () => {
|
|
112
|
+
const wrapper = mount(RcSection, {
|
|
113
|
+
props: { ...defaultProps },
|
|
114
|
+
slots: { title: '<span class="custom-title">Custom</span>' },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(wrapper.find('.custom-title').exists()).toBe(true);
|
|
118
|
+
expect(wrapper.find('.custom-title').text()).toBe('Custom');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('expandable behavior', () => {
|
|
123
|
+
it('should render toggle button when expandable is true', () => {
|
|
124
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
|
|
125
|
+
|
|
126
|
+
expect(wrapper.find('.toggle-button').exists()).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should not render toggle button when expandable is false', () => {
|
|
130
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
|
|
131
|
+
|
|
132
|
+
expect(wrapper.find('.toggle-button').exists()).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should set aria-expanded on toggle button when expandable', () => {
|
|
136
|
+
const wrapper = mount(RcSection, {
|
|
137
|
+
props: {
|
|
138
|
+
...defaultProps, expandable: true, expanded: true
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('true');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should set aria-expanded="false" on toggle button when collapsed', () => {
|
|
146
|
+
const wrapper = mount(RcSection, {
|
|
147
|
+
props: {
|
|
148
|
+
...defaultProps, expandable: true, expanded: false
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('false');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should set aria-label to "Collapse section" on toggle button when expanded', () => {
|
|
156
|
+
const wrapper = mount(RcSection, {
|
|
157
|
+
props: {
|
|
158
|
+
...defaultProps, expandable: true, expanded: true
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Collapse section');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should set aria-label to "Expand section" on toggle button when collapsed', () => {
|
|
166
|
+
const wrapper = mount(RcSection, {
|
|
167
|
+
props: {
|
|
168
|
+
...defaultProps, expandable: true, expanded: false
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Expand section');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should emit update:expanded with false when clicking an expanded header', async() => {
|
|
176
|
+
const wrapper = mount(RcSection, {
|
|
177
|
+
props: {
|
|
178
|
+
...defaultProps, expandable: true, expanded: true
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await wrapper.find('.section-header').trigger('click');
|
|
183
|
+
|
|
184
|
+
expect(wrapper.emitted('update:expanded')).toHaveLength(1);
|
|
185
|
+
expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should emit update:expanded with true when clicking a collapsed header', async() => {
|
|
189
|
+
const wrapper = mount(RcSection, {
|
|
190
|
+
props: {
|
|
191
|
+
...defaultProps, expandable: true, expanded: false
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await wrapper.find('.section-header').trigger('click');
|
|
196
|
+
|
|
197
|
+
expect(wrapper.emitted('update:expanded')).toHaveLength(1);
|
|
198
|
+
expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([true]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should not emit update:expanded when clicking a non-expandable header', async() => {
|
|
202
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
|
|
203
|
+
|
|
204
|
+
await wrapper.find('.section-header').trigger('click');
|
|
205
|
+
|
|
206
|
+
expect(wrapper.emitted('update:expanded')).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should emit update:expanded when toggle button is clicked', async() => {
|
|
210
|
+
const wrapper = mount(RcSection, {
|
|
211
|
+
props: {
|
|
212
|
+
...defaultProps, expandable: true, expanded: true
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await wrapper.find('.toggle-button').trigger('click');
|
|
217
|
+
|
|
218
|
+
expect(wrapper.emitted('update:expanded')).toHaveLength(1);
|
|
219
|
+
expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('expanded prop', () => {
|
|
224
|
+
it('should default expanded to true', () => {
|
|
225
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
|
|
226
|
+
|
|
227
|
+
expect(wrapper.find('.section-content').exists()).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should render content when expanded is true', () => {
|
|
231
|
+
const wrapper = mount(RcSection, {
|
|
232
|
+
props: { ...defaultProps, expanded: true },
|
|
233
|
+
slots: { default: '<p>Content</p>' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(wrapper.find('.section-content').exists()).toBe(true);
|
|
237
|
+
expect(wrapper.find('p').text()).toBe('Content');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should hide content when expanded is false', () => {
|
|
241
|
+
const wrapper = mount(RcSection, {
|
|
242
|
+
props: { ...defaultProps, expanded: false },
|
|
243
|
+
slots: { default: '<p>Content</p>' },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(wrapper.find('.section-content').exists()).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should apply expandable-content class when expandable is true', () => {
|
|
250
|
+
const wrapper = mount(RcSection, {
|
|
251
|
+
props: {
|
|
252
|
+
...defaultProps, expandable: true, expanded: true
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(wrapper.find('.section-content').classes()).toContain('expandable-content');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should not apply expandable-content class when expandable is false', () => {
|
|
260
|
+
const wrapper = mount(RcSection, {
|
|
261
|
+
props: {
|
|
262
|
+
...defaultProps, expandable: false, expanded: true
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(wrapper.find('.section-content').classes()).not.toContain('expandable-content');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should add collapsed class to header when not expanded', () => {
|
|
270
|
+
const wrapper = mount(RcSection, {
|
|
271
|
+
props: {
|
|
272
|
+
...defaultProps, expandable: true, expanded: false
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(wrapper.find('.section-header').classes()).toContain('collapsed');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('slots', () => {
|
|
281
|
+
it('should render badges slot inside right-wrapper', () => {
|
|
282
|
+
const wrapper = mount(RcSection, {
|
|
283
|
+
props: { ...defaultProps },
|
|
284
|
+
slots: { badges: '<span class="test-badge">Badge</span>' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(wrapper.find('.right-wrapper .status-badges .test-badge').exists()).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should render actions slot inside right-wrapper', () => {
|
|
291
|
+
const wrapper = mount(RcSection, {
|
|
292
|
+
props: { ...defaultProps },
|
|
293
|
+
slots: { actions: '<button class="test-action">Act</button>' },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(wrapper.find('.right-wrapper .actions .test-action').exists()).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should not render right-wrapper when no badges or actions slots', () => {
|
|
300
|
+
const wrapper = mount(RcSection, { props: { ...defaultProps } });
|
|
301
|
+
|
|
302
|
+
expect(wrapper.find('.right-wrapper').exists()).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should render counter slot', () => {
|
|
306
|
+
const wrapper = mount(RcSection, {
|
|
307
|
+
props: { ...defaultProps },
|
|
308
|
+
slots: { counter: '<span class="test-counter">5</span>' },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(wrapper.find('.test-counter').exists()).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should render errors slot', () => {
|
|
315
|
+
const wrapper = mount(RcSection, {
|
|
316
|
+
props: { ...defaultProps },
|
|
317
|
+
slots: { errors: '<span class="test-error">!</span>' },
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(wrapper.find('.test-error').exists()).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A section container used for grouping and organizing content, with an
|
|
4
|
+
* optional header that supports title, badges, actions, and expandability.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
*
|
|
8
|
+
* <RcSection title="Section title" type="secondary" mode="with-header" :expandable="false" background="secondary">
|
|
9
|
+
* <p>Section content here</p>
|
|
10
|
+
* </RcSection>
|
|
11
|
+
*
|
|
12
|
+
* <RcSection title="Section title" type="secondary" mode="with-header" expandable v-model:expanded="expanded" background="secondary">
|
|
13
|
+
* <template #counter>
|
|
14
|
+
* <RcCounterBadge :count="99" type="inactive" />
|
|
15
|
+
* </template>
|
|
16
|
+
* <template #errors>
|
|
17
|
+
* <RcIcon v-clean-tooltip="'3 validation errors'" type="error" size="large" status="error" />
|
|
18
|
+
* </template>
|
|
19
|
+
* <template #badges>
|
|
20
|
+
* <RcSectionBadges :badges="[
|
|
21
|
+
* { label: 'Status', status: 'success', tooltip: 'All systems operational' },
|
|
22
|
+
* { label: 'Status', status: 'warning', tooltip: 'Degraded performance' },
|
|
23
|
+
* { label: 'Status', status: 'error', tooltip: 'Service unavailable' },
|
|
24
|
+
* ]" />
|
|
25
|
+
* </template>
|
|
26
|
+
* <template #actions>
|
|
27
|
+
* <RcSectionActions :actions="[
|
|
28
|
+
* { label: 'Action', icon: 'chevron-left', action: () => {} },
|
|
29
|
+
* { icon: 'copy', ariaLabel: 'Copy', action: () => {} },
|
|
30
|
+
* { icon: 'trash', label: 'Delete', action: () => {} },
|
|
31
|
+
* ]" />
|
|
32
|
+
* </template>
|
|
33
|
+
* <p>Section content here</p>
|
|
34
|
+
* </RcSection>
|
|
35
|
+
*/
|
|
36
|
+
import { computed, inject, provide, type Ref } from 'vue';
|
|
37
|
+
import RcButton from '@components/RcButton/RcButton.vue';
|
|
38
|
+
import RcIcon from '@components/RcIcon/RcIcon.vue';
|
|
39
|
+
import type { RcSectionProps, SectionBackground } from './types';
|
|
40
|
+
|
|
41
|
+
const RC_SECTION_BG_KEY = 'rc-section-background';
|
|
42
|
+
|
|
43
|
+
const props = withDefaults(defineProps<RcSectionProps>(), { title: '' });
|
|
44
|
+
|
|
45
|
+
const parentBackground = inject<Ref<SectionBackground> | null>(RC_SECTION_BG_KEY, null);
|
|
46
|
+
|
|
47
|
+
const resolvedBackground = computed<SectionBackground>(() => {
|
|
48
|
+
if (props.background) {
|
|
49
|
+
return props.background;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parent = parentBackground?.value ?? null;
|
|
53
|
+
|
|
54
|
+
return parent === 'primary' ? 'secondary' : 'primary';
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
provide(RC_SECTION_BG_KEY, resolvedBackground);
|
|
58
|
+
|
|
59
|
+
const expanded = defineModel<boolean>('expanded', { default: true });
|
|
60
|
+
|
|
61
|
+
const hasHeader = computed(() => {
|
|
62
|
+
return props.mode === 'with-header';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sectionClass = computed(() => ({
|
|
66
|
+
'rc-section': true,
|
|
67
|
+
'type-primary': props.type === 'primary',
|
|
68
|
+
'type-secondary': props.type === 'secondary',
|
|
69
|
+
'bg-primary': resolvedBackground.value === 'primary',
|
|
70
|
+
'bg-secondary': resolvedBackground.value === 'secondary',
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
const contentClass = computed(() => ({
|
|
74
|
+
'section-content': true,
|
|
75
|
+
'no-header': !hasHeader.value,
|
|
76
|
+
'expandable-content': props.expandable,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
function toggle() {
|
|
80
|
+
if (props.expandable) {
|
|
81
|
+
expanded.value = !expanded.value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<template>
|
|
87
|
+
<div :class="sectionClass">
|
|
88
|
+
<div
|
|
89
|
+
v-if="hasHeader"
|
|
90
|
+
class="section-header"
|
|
91
|
+
:class="{ expandable: props.expandable, collapsed: !expanded }"
|
|
92
|
+
@click="toggle"
|
|
93
|
+
>
|
|
94
|
+
<div class="left-wrapper">
|
|
95
|
+
<RcButton
|
|
96
|
+
v-if="props.expandable"
|
|
97
|
+
class="toggle-button"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
:aria-expanded="expanded"
|
|
100
|
+
:aria-label="expanded ? 'Collapse section' : 'Expand section'"
|
|
101
|
+
@click.stop="toggle"
|
|
102
|
+
>
|
|
103
|
+
<RcIcon
|
|
104
|
+
:type="expanded ? 'chevron-down' : 'chevron-right'"
|
|
105
|
+
size="medium"
|
|
106
|
+
/>
|
|
107
|
+
</RcButton>
|
|
108
|
+
<div class="title">
|
|
109
|
+
<slot name="title">
|
|
110
|
+
{{ props.title }}
|
|
111
|
+
</slot>
|
|
112
|
+
<slot name="counter" />
|
|
113
|
+
<slot name="errors" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div
|
|
117
|
+
v-if="$slots.badges || $slots.actions"
|
|
118
|
+
class="right-wrapper"
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
v-if="$slots.badges"
|
|
122
|
+
class="status-badges"
|
|
123
|
+
>
|
|
124
|
+
<slot name="badges" />
|
|
125
|
+
</div>
|
|
126
|
+
<div
|
|
127
|
+
v-if="$slots.actions"
|
|
128
|
+
class="actions"
|
|
129
|
+
@click.stop
|
|
130
|
+
>
|
|
131
|
+
<slot name="actions" />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div
|
|
136
|
+
v-if="expanded"
|
|
137
|
+
:class="contentClass"
|
|
138
|
+
>
|
|
139
|
+
<slot />
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<style lang="scss" scoped>
|
|
145
|
+
.rc-section {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
|
|
150
|
+
&.type-primary {
|
|
151
|
+
.title {
|
|
152
|
+
font-weight: 700;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
&.type-secondary {
|
|
157
|
+
padding: 0 16px;
|
|
158
|
+
border-radius: 8px;
|
|
159
|
+
|
|
160
|
+
.title {
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&.bg-primary {
|
|
166
|
+
background-color: var(--rc-section-background-primary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
&.bg-secondary {
|
|
170
|
+
background-color: var(--rc-section-background-secondary);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.section-header {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: row;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 24px;
|
|
179
|
+
height: 56px;
|
|
180
|
+
|
|
181
|
+
&.expandable {
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
user-select: none;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.left-wrapper {
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: row;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 12px;
|
|
192
|
+
|
|
193
|
+
.toggle-button + .title {
|
|
194
|
+
margin-left: -4px;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.title {
|
|
199
|
+
display: inline-flex;
|
|
200
|
+
gap: 12px;
|
|
201
|
+
font-size: 18px;
|
|
202
|
+
line-height: 1.2;
|
|
203
|
+
color: var(--body-text, inherit);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
button.btn-medium.toggle-button {
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
font-size: 16px;
|
|
209
|
+
color: var(--body-text, inherit);
|
|
210
|
+
padding: 0;
|
|
211
|
+
min-height: initial;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.right-wrapper {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: row;
|
|
217
|
+
justify-content: flex-end;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 24px;
|
|
220
|
+
margin-left: auto;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.status-badges {
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: row;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 12px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.actions {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: row;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.section-content {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 24px;
|
|
241
|
+
padding: 0 0 16px;
|
|
242
|
+
color: var(--body-text);
|
|
243
|
+
|
|
244
|
+
&.expandable-content {
|
|
245
|
+
padding: 0 0 16px 24px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&.no-header {
|
|
249
|
+
padding: 16px 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
</style>
|