refactor(editor): Move editor-ui and design-system to frontend dir (no-changelog) (#13564)

This commit is contained in:
Alex Grozav
2025-02-28 14:28:30 +02:00
committed by GitHub
parent 684353436d
commit f5743176e5
1635 changed files with 805 additions and 1079 deletions

View File

@@ -0,0 +1,192 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { StoryFn } from '@storybook/vue3';
import UserStack from './UserStack.vue';
export default {
title: 'Modules/UserStack',
component: UserStack,
};
const Template: StoryFn = (args) => ({
setup: () => ({ args }),
props: args,
components: {
UserStack,
},
template: '<n8n-user-stack v-bind="args" />',
});
export const WithGroups = Template.bind({});
WithGroups.args = {
currentUserEmail: 'sunny@n8n.io',
users: {
Owners: [
{
id: '1',
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: 'sunny@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: true,
signInType: 'email',
disabled: false,
},
{
id: '2',
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: 'kobi@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
],
'Other users': [
{
id: '3',
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe',
email: 'john@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'email',
disabled: false,
},
{
id: '4',
firstName: 'Jane',
lastName: 'Doe',
fullName: 'Jane Doe',
email: 'jane@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
{
id: '5',
firstName: 'Test',
lastName: 'User',
fullName: 'Test User',
email: 'test@n8n.io',
isDefaultUser: false,
isPendingUser: true,
isOwner: false,
signInType: 'email',
disabled: false,
},
],
'Empty Group': [],
},
};
export const SingleGroup = Template.bind({});
SingleGroup.args = {
currentUserEmail: 'sunny@n8n.io',
users: {
Owners: [
{
id: '1',
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: 'sunny@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: true,
signInType: 'email',
disabled: false,
},
{
id: '2',
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: 'kobi@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
{
id: '4',
firstName: 'Jane',
lastName: 'Doe',
fullName: 'Jane Doe',
email: 'jane@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
{
id: '5',
firstName: 'Test',
lastName: 'User',
fullName: 'Test User',
email: 'test@n8n.io',
isDefaultUser: false,
isPendingUser: true,
isOwner: false,
signInType: 'email',
disabled: false,
},
],
},
};
export const NoCutoff = Template.bind({});
NoCutoff.args = {
currentUserEmail: 'sunny@n8n.io',
users: {
Owners: [
{
id: '1',
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: 'sunny@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: true,
signInType: 'email',
disabled: false,
},
{
id: '2',
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: 'kobi@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
{
id: '3',
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe',
email: 'john@n8n.io',
isDefaultUser: false,
isPendingUser: false,
isOwner: false,
signInType: 'email',
disabled: false,
},
],
},
};

View File

@@ -0,0 +1,113 @@
import { render } from '@testing-library/vue';
import UserStack from './UserStack.vue';
import { N8nAvatar, N8nUserInfo } from '../index';
describe('UserStack', () => {
it('should render flat user list', () => {
const { container } = render(UserStack, {
props: {
currentUserEmail: 'hello@n8n.io',
users: {
Owners: [
{
id: '1',
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: 'hello@n8n.io',
isPendingUser: false,
isOwner: true,
signInType: 'email',
disabled: false,
},
{
id: '2',
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: 'kobi@n8n.io',
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
],
},
},
global: {
components: {
'n8n-avatar': N8nAvatar,
'n8n-user-info': N8nUserInfo,
},
},
});
expect(container.querySelector('.user-stack')).toBeInTheDocument();
expect(container.querySelectorAll('svg')).toHaveLength(2);
});
it('should not show all avatars', async () => {
const { container } = render(UserStack, {
props: {
currentUserEmail: 'hello@n8n.io',
users: {
Owners: [
{
id: '1',
firstName: 'Sunny',
lastName: 'Side',
fullName: 'Sunny Side',
email: 'hello@n8n.io',
isPendingUser: false,
isOwner: true,
signInType: 'email',
disabled: false,
},
{
id: '2',
firstName: 'Kobi',
lastName: 'Dog',
fullName: 'Kobi Dog',
email: 'kobi@n8n.io',
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
{
id: '3',
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe',
email: 'john@n8n.io',
isPendingUser: false,
isOwner: false,
signInType: 'email',
disabled: false,
},
{
id: '4',
firstName: 'Jane',
lastName: 'Doe',
fullName: 'Jane Doe',
email: 'jane@n8n.io',
isPendingUser: false,
isOwner: false,
signInType: 'ldap',
disabled: true,
},
],
},
},
global: {
components: {
'n8n-avatar': N8nAvatar,
'n8n-user-info': N8nUserInfo,
},
},
});
expect(container.querySelector('.user-stack')).toBeInTheDocument();
expect(container.querySelectorAll('svg')).toHaveLength(2);
expect(container.querySelector('.hiddenBadge')).toHaveTextContent('+2');
});
});

View File

@@ -0,0 +1,187 @@
<script lang="ts" setup>
import { computed } from 'vue';
import type { IUser, UserStackGroups } from '@n8n/design-system/types';
import N8nAvatar from '../N8nAvatar';
import N8nUserInfo from '../N8nUserInfo';
const props = withDefaults(
defineProps<{
users: UserStackGroups;
currentUserEmail?: string;
maxAvatars?: number;
dropdownTrigger?: 'hover' | 'click';
}>(),
{
currentUserEmail: '',
maxAvatars: 2,
dropdownTrigger: 'hover',
},
);
const nonEmptyGroups = computed(() => {
const users: UserStackGroups = {};
for (const groupName in props.users) {
if (props.users[groupName].length > 0) {
users[groupName] = props.users[groupName];
}
}
return users;
});
const groupCount = computed(() => {
return Object.keys(nonEmptyGroups.value).length;
});
const flatUserList = computed(() => {
const users: IUser[] = [];
for (const groupName in props.users) {
users.push(...props.users[groupName]);
}
return users;
});
const visibleAvatarCount = computed(() => {
return flatUserList.value.length >= props.maxAvatars
? props.maxAvatars
: flatUserList.value.length;
});
const hiddenUsersCount = computed(() => {
return flatUserList.value.length - visibleAvatarCount.value;
});
const menuHeight = computed(() => {
return groupCount.value > 1 ? 220 : 190;
});
</script>
<template>
<div class="user-stack" data-test-id="user-stack-container">
<el-dropdown
:trigger="$props.dropdownTrigger"
:max-height="menuHeight"
popper-class="user-stack-popper"
>
<div :class="$style.avatars" data-test-id="user-stack-avatars">
<N8nAvatar
v-for="user in flatUserList.slice(0, visibleAvatarCount)"
:key="user.id"
:first-name="user.firstName"
:last-name="user.lastName"
:class="$style.avatar"
:data-test-id="`user-stack-avatar-${user.id}`"
size="small"
/>
<div v-if="hiddenUsersCount > 0" :class="$style.hiddenBadge">+{{ hiddenUsersCount }}</div>
</div>
<template #dropdown>
<el-dropdown-menu class="user-stack-list" data-test-id="user-stack-list">
<div v-for="(groupUsers, index) in nonEmptyGroups" :key="index">
<div :class="$style.groupContainer">
<el-dropdown-item>
<header v-if="groupCount > 1" :class="$style.groupName">{{ index }}</header>
</el-dropdown-item>
<div :class="$style.groupUsers">
<el-dropdown-item
v-for="user in groupUsers"
:key="user.id"
:data-test-id="`user-stack-info-${user.id}`"
:class="$style.userInfoContainer"
>
<N8nUserInfo
v-bind="user"
:is-current-user="user.email === props.currentUserEmail"
/>
</el-dropdown-item>
</div>
</div>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<style lang="scss" module>
.avatars {
display: flex;
cursor: pointer;
}
.avatar {
margin-right: calc(-1 * var(--spacing-3xs));
user-select: none;
}
.hiddenBadge {
display: flex;
justify-content: center;
align-items: center;
width: 28px;
height: 28px;
color: var(--color-text-base);
background-color: var(--color-background-xlight);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-3xs);
z-index: 999;
border: var(--border-width-base) var(--border-style-base) var(--color-info-tint-1);
border-radius: 50%;
}
.groupContainer {
display: flex;
flex-direction: column;
& + & {
margin-top: var(--spacing-3xs);
}
}
.groupName {
font-size: var(--font-size-3xs);
color: var(--color-text-light);
text-transform: uppercase;
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-4xs);
}
.groupUsers {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
}
.userInfoContainer {
display: flex;
padding-top: var(--spacing-5xs);
padding-bottom: var(--spacing-5xs);
}
</style>
<style lang="scss">
ul.user-stack-list {
border: none;
display: flex;
flex-direction: column;
gap: var(--spacing-s);
padding-bottom: var(--spacing-2xs);
.el-dropdown-menu__item {
line-height: var(--font-line-height-regular);
}
li:hover {
color: currentColor !important;
}
}
.user-stack-popper {
border: 1px solid var(--border-color-light);
border-radius: var(--border-radius-base);
padding: var(--spacing-5xs) 0;
box-shadow: 0px 2px 8px 0px #441c171a;
background-color: var(--color-background-xlight);
}
</style>

View File

@@ -0,0 +1,3 @@
import N8nUserStack from './UserStack.vue';
export default N8nUserStack;