feat(editor): Alert design system component (#4834)

* feat(editor): Alert component

* feat(editor): style improvements
This commit is contained in:
Csaba Tuncsik 2022-12-06 16:51:46 +01:00 committed by GitHub
parent 7b819ad5a1
commit 9dbb3ea182
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 349 additions and 0 deletions

View file

@ -0,0 +1,65 @@
import type { StoryFn } from '@storybook/vue';
import N8nAlert from './Alert.vue';
import N8nIcon from '../N8nIcon';
export default {
title: 'Atoms/Alert',
component: N8nAlert,
argTypes: {
type: {
type: 'select',
options: ['success', 'info', 'warning', 'error'],
},
effect: {
type: 'select',
options: ['light', 'dark'],
},
},
};
const Template: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nAlert,
},
template:
'<div style="position: relative; width: 100%; height: 300px;"><n8n-alert v-bind="$props"><template #aside>custom content slot</template></n8n-alert></div>',
});
export const ContentAsProps = Template.bind({});
ContentAsProps.args = {
type: 'info',
effect: 'light',
title: 'Alert title',
description: 'Alert description',
center: false,
showIcon: true,
background: true,
};
const TemplateForSlots: StoryFn = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nAlert,
N8nIcon,
},
template: `<div style="position: relative; width: 100%; height: 300px;">
<n8n-alert v-bind="$props">
<template #title>Title</template>
<template>Description</template>
<template #aside><button>Button</button></template>
<template #icon>
<n8n-icon icon="grin-stars" size="xlarge" />
</template>
</n8n-alert>
</div>`,
});
export const ContentInSlots = TemplateForSlots.bind({});
ContentInSlots.args = {
type: 'info',
effect: 'light',
center: false,
background: true,
showIcon: false,
};

View file

@ -0,0 +1,242 @@
<template>
<div :class="alertBoxClassNames" role="alert">
<div :class="$style.content">
<span v-if="showIcon || $slots.icon" :class="$style.icon">
<n8n-icon v-if="showIcon" :icon="icon" />
<slot v-else-if="$slots.icon" name="icon" />
</span>
<div :class="$style.text">
<div v-if="$slots.title || title" :class="$style.title">
<slot name="title">{{ title }}</slot>
</div>
<div v-if="$slots.default || description" :class="$style.description">
<slot>{{ description }}</slot>
</div>
</div>
</div>
<div v-if="$slots.aside" :class="$style.aside">
<slot name="aside" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nIcon from '../N8nIcon';
type AlertProps = {
title?: string;
type?: 'success' | 'warning' | 'info' | 'error';
description?: string;
center?: boolean;
showIcon?: boolean;
effect?: 'light' | 'dark';
background?: boolean;
};
const props = withDefaults(defineProps<AlertProps>(), {
type: 'info',
effect: 'light',
showIcon: true,
background: true,
});
const icon = computed(() => {
/* eslint-disable prettier/prettier */
switch (props.type) {
case 'success':
return 'check-circle';
case 'warning':
return 'exclamation-triangle';
case 'error':
return 'times-circle';
default:
return 'info-circle';
}
/* eslint-enable */
});
const style = useCssModule();
const alertBoxClassNames = computed(() => {
const classNames = ['n8n-alert', style.alert];
if (props.type) {
classNames.push(style[props.type]);
}
if (props.effect) {
classNames.push(style[props.effect]);
}
if (props.center) {
classNames.push(style.center);
}
if (props.background) {
classNames.push(style.background);
}
return classNames;
});
</script>
<style lang="scss" module>
@import '../../css/common/var.scss';
.alert {
display: flex;
position: relative;
min-height: 60px;
border-bottom: 1px solid transparent;
align-items: center;
justify-content: space-between;
padding: $alert-padding;
&.center {
justify-content: center;
}
&.success {
&.light {
color: var(--color-success);
&.background {
background-color: $color-success-lighter;
border-color: var(--color-success);
}
.el-alert__description {
color: var(--color-success);
}
}
&.dark {
color: $color-white;
&:not(.background) {
color: var(--color-success);
}
&.background {
background-color: var(--color-success);
border-color: $color-white;
}
}
}
&.info {
&.light {
color: var(--color-info);
&.background {
background-color: $alert-info-color;
border-color: var(--color-info);
}
}
&.dark {
color: $color-white;
&:not(.background) {
color: var(--color-info);
}
&.background {
background-color: var(--color-info);
border-color: $color-white;
}
}
.el-alert__description {
color: var(--color-info);
}
}
&.warning {
&.light {
color: var(--color-warning);
&.background {
background-color: $alert-warning-color;
border-color: var(--color-warning);
}
.el-alert__description {
color: var(--color-warning);
}
}
&.dark {
color: $color-white;
&:not(.background) {
color: var(--color-warning);
}
&.background {
background-color: var(--color-warning);
border-color: $color-white;
}
}
}
&.error {
&.light {
color: var(--color-danger);
&.background {
background-color: $alert-danger-color;
border-color: var(--color-danger);
}
.el-alert__description {
color: var(--color-danger);
}
}
&.dark {
color: $color-white;
&:not(.background) {
color: var(--color-danger);
}
&.background {
background-color: var(--color-danger);
border-color: $color-white;
}
}
}
}
.content {
display: inline-flex;
align-items: center;
}
.icon {
display: inline-flex;
color: inherit;
align-items: center;
padding-left: var(--spacing-2xs);
padding-right: var(--spacing-s);
}
.text {
display: inline-flex;
flex-direction: column;
justify-content: center;
}
.title {
font-size: $alert-title-font-size;
line-height: 18px;
font-weight: bold;
}
.description {
font-size: $alert-description-font-size;
margin: 5px 0 0 0;
}
.aside {
display: inline-flex;
align-items: center;
padding-left: var(--spacing-s);
}
</style>

View file

@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/vue';
import N8nAlert from '../Alert.vue';
import N8nIcon from '../../N8nIcon';
describe('components', () => {
describe('N8nAlert', () => {
it('should render with props', () => {
render(N8nAlert, {
props: { title: 'Title', description: 'Message' },
});
expect(screen.getByRole('alert')).toBeVisible();
expect(screen.getByText('Title')).toBeVisible();
expect(screen.getByText('Message')).toBeVisible();
});
it('should render slots instead of props', () => {
const { container } = render(
N8nAlert,
{
props: { showIcon: false },
slots: {
title: 'Title',
default: 'Message',
aside: '<button>Click me</button>',
icon: '<n8n-icon icon="plus-circle" />',
},
},
(localVue) => {
localVue.component('n8n-icon', N8nIcon);
},
);
expect(screen.getByRole('alert')).toBeVisible();
expect(screen.getByText('Title')).toBeVisible();
expect(screen.getByText('Message')).toBeVisible();
expect(screen.getByRole('button')).toBeVisible();
expect(container.querySelector('.n8n-icon')).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1 @@
export { default } from './Alert.vue';

View file

@ -2,6 +2,7 @@ import Vue from 'vue';
import N8nActionBox from '../components/N8nActionBox';
import N8nActionDropdown from '../components/N8nActionDropdown';
import N8nActionToggle from '../components/N8nActionToggle';
import N8nAlert from '../components/N8nAlert';
import N8nAvatar from '../components/N8nAvatar';
import N8nBadge from '../components/N8nBadge';
import N8nBlockUi from '../components/N8nBlockUi';
@ -50,6 +51,7 @@ export default {
app.component('n8n-action-box', N8nActionBox);
app.component('n8n-action-dropdown', N8nActionDropdown);
app.component('n8n-action-toggle', N8nActionToggle);
app.component('n8n-alert', N8nAlert);
app.component('n8n-avatar', N8nAvatar);
app.component('n8n-badge', N8nBadge);
app.component('n8n-block-ui', N8nBlockUi);