diff --git a/packages/design-system/src/components/N8nMenu/Menu.stories.js b/packages/design-system/src/components/N8nMenu/Menu.stories.js deleted file mode 100644 index b7cab35354cb5..0000000000000 --- a/packages/design-system/src/components/N8nMenu/Menu.stories.js +++ /dev/null @@ -1,46 +0,0 @@ -import N8nMenu from './Menu.vue'; -import N8nMenuItem from '../N8nMenuItem'; - -import { action } from '@storybook/addon-actions'; - -export default { - title: 'Atoms/Menu', - component: N8nMenu, - argTypes: { - type: { - control: 'select', - options: ['primary', 'secondary'], - }, - }, - parameters: { - backgrounds: { default: '--color-background-xlight' }, - }, -}; - -const methods = { - onSelect: action('select'), -}; - -const Template = (args, { argTypes }) => ({ - props: Object.keys(argTypes), - components: { - N8nMenu, - N8nMenuItem, - }, - template: - ` - Item 1 - Item 2 - `, - methods, -}); - -export const Primary = Template.bind({}); -Primary.parameters = { - backgrounds: { default: '--color-background-light' }, -}; - -export const Secondary = Template.bind({}); -Secondary.args = { - type: 'secondary', -}; diff --git a/packages/design-system/src/components/N8nMenu/Menu.stories.ts b/packages/design-system/src/components/N8nMenu/Menu.stories.ts new file mode 100644 index 0000000000000..fd38c6ae8486d --- /dev/null +++ b/packages/design-system/src/components/N8nMenu/Menu.stories.ts @@ -0,0 +1,160 @@ +import N8nMenu from './Menu.vue'; +import N8nIcon from '../N8nIcon'; +import N8nText from '../N8nText'; +import { StoryFn } from '@storybook/vue'; +import { action } from '@storybook/addon-actions'; + +export default { + title: 'Atoms/Menu', + component: N8nMenu, + argTypes: { + }, +}; + +const methods = { + onSelect: action('select'), +}; + +const template: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nMenu, + }, + template: ` +
+ +
+ `, + methods, +}); + +const templateWithHeaderAndFooter: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nMenu, + N8nIcon, + N8nText, + }, + template: ` +
+ + + + +
+ `, + methods, +}); + +const templateWithAllSlots: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nMenu, + N8nIcon, + N8nText, + }, + template: ` +
+ + + + + + +
+ `, + methods, +}); + +const menuItems = [ + { + id: 'workflows', + icon: 'network-wired', + label: 'Workflows', + position: 'top', + }, + { + id: 'executions', + icon: 'tasks', + label: 'Executions', + position: 'top', + }, + { + id: 'disabled-item', + icon: 'times', + label: 'Not Available', + available: false, + position: 'top', + }, + { + id: 'website', + icon: 'globe', + label: 'Website', + type: 'link', + properties: { + href: 'https://www.n8n.io', + newWindow: true, + }, + position: 'bottom', + }, + { + id: 'help', + icon: 'question', + label: 'Help', + position: 'bottom', + children: [ + { icon: 'info', label: 'About n8n', id: 'about' }, + { icon: 'book', label: 'Documentation', id: 'docs' }, + { + id: 'disabled-submenu-item', + icon: 'times', + label: 'Not Available', + available: false, + position: 'top', + }, + { + id: 'quickstart', + icon: 'video', + label: 'Quickstart', + type: 'link', + properties: { + href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', + newWindow: true, + }, + }, + ], + }, +]; + +export const primary = template.bind({}); +primary.args = { + items: menuItems, +}; + +export const withHeaderAndFooter = templateWithHeaderAndFooter.bind({}); +withHeaderAndFooter.args = { items: menuItems }; + +export const withAllSlots = templateWithAllSlots.bind({}); +withAllSlots.args = { items: menuItems }; diff --git a/packages/design-system/src/components/N8nMenu/Menu.vue b/packages/design-system/src/components/N8nMenu/Menu.vue index 72a1a1b976340..814558c9ec02b 100644 --- a/packages/design-system/src/components/N8nMenu/Menu.vue +++ b/packages/design-system/src/components/N8nMenu/Menu.vue @@ -1,22 +1,83 @@ diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts b/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts new file mode 100644 index 0000000000000..7d185ae472d9f --- /dev/null +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.stories.ts @@ -0,0 +1,79 @@ +import N8nMenuItem from "."; +import ElMenu from 'element-ui/lib/menu'; +import { StoryFn } from '@storybook/vue'; + +export default { + title: 'Atoms/MenuItem', + component: N8nMenuItem, +}; + +const template: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment + N8nMenuItem , + }, + template: ` +
+ + + +
+ `, +}); + +export const defaultMenuItem = template.bind({}); +defaultMenuItem.args = { + item: { + id: 'workflows', + icon: 'heart', + label: 'Workflows', + }, +}; + +export const compact = template.bind({}); +compact.args = { + item: { + id: 'compact', + icon: 'ice-cream', + label: 'Click here', + }, + compact: true, +}; + +export const link = template.bind({}); +link.args = { + item: { + id: 'website', + icon: 'globe', + label: 'Website', + type: 'link', + properties: { + href: 'https://www.n8n.io', + newWindow: true, + }, + }, +}; + +export const withChildren = template.bind({}); +withChildren.args = { + item: { + id: 'help', + icon: 'question', + label: 'Help', + children: [ + { icon: 'info', label: 'About n8n', id: 'about' }, + { icon: 'book', label: 'Documentation', id: 'docs' }, + { + id: 'quickstart', + icon: 'video', + label: 'Quickstart', + type: 'link', + properties: { + href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok', + newWindow: true, + }, + }, + ], + }, +}; diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue index 0960fc3940bb2..7affae987f842 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -1,7 +1,272 @@ + + + + diff --git a/packages/design-system/src/types/index.ts b/packages/design-system/src/types/index.ts index a9602f5874b96..4e7d31c6e68d7 100644 --- a/packages/design-system/src/types/index.ts +++ b/packages/design-system/src/types/index.ts @@ -1,3 +1,4 @@ export * from './form'; export * from './user'; +export * from './menu'; export * from './button'; diff --git a/packages/design-system/src/types/menu.ts b/packages/design-system/src/types/menu.ts new file mode 100644 index 0000000000000..b66b2f8c50483 --- /dev/null +++ b/packages/design-system/src/types/menu.ts @@ -0,0 +1,21 @@ +export type IMenuItem = { + id: string; + label: string; + icon?: string; + customIconSize?: 'medium' | 'small'; + available?: boolean; + position?: 'top' | 'bottom'; + type?: 'default' | 'link'; + properties?: ILinkMenuItemProperties; + // For router menus populate only one of those arrays: + // If menu item can be activated on certain route names (easy mode) + activateOnRouteNames?: string[], + // For more specific matching, we can use paths + activateOnRoutePaths?: string[], + children?: IMenuItem[], +}; + +export type ILinkMenuItemProperties = { + href: string; + newWindow?: boolean; +}; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index c38766fd29f45..747435c9acb52 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -754,23 +754,6 @@ export interface ITimeoutHMS { export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR'; -export type MenuItemType = 'link'; -export type MenuItemPosition = 'top' | 'bottom'; - -export interface IMenuItem { - id: string; - type: MenuItemType; - position?: MenuItemPosition; - properties: ILinkMenuItemProperties; -} - -export interface ILinkMenuItemProperties { - title: string; - icon: string; - href: string; - newWindow?: boolean; -} - export interface ISubcategoryItemProps { subcategory: string; description: string; diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 851eec8503d00..4a52a9cde7183 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -49,34 +49,7 @@ - - diff --git a/packages/editor-ui/src/components/MenuItemsIterator.vue b/packages/editor-ui/src/components/MenuItemsIterator.vue deleted file mode 100644 index eaee86c78f928..0000000000000 --- a/packages/editor-ui/src/components/MenuItemsIterator.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/packages/editor-ui/src/components/SettingsSidebar.vue b/packages/editor-ui/src/components/SettingsSidebar.vue index 9ade642415dd1..35ccc86cefc8d 100644 --- a/packages/editor-ui/src/components/SettingsSidebar.vue +++ b/packages/editor-ui/src/components/SettingsSidebar.vue @@ -1,53 +1,22 @@ @@ -59,6 +28,8 @@ import { userHelpers } from './mixins/userHelpers'; import { pushConnection } from "@/components/mixins/pushConnection"; import { IFakeDoor } from '@/Interface'; import GiftNotificationIcon from './GiftNotificationIcon.vue'; +import { IMenuItem } from 'n8n-design-system'; +import { BaseTextKey } from '@/plugins/i18n'; export default mixins( userHelpers, @@ -73,6 +44,64 @@ export default mixins( settingsFakeDoorFeatures(): IFakeDoor[] { return this.$store.getters['ui/getFakeDoorByLocation']('settings'); }, + sidebarMenuItems(): IMenuItem[] { + + const menuItems: IMenuItem[] = [ + { + id: 'settings-personal', + icon: 'user-circle', + label: this.$locale.baseText('settings.personal'), + position: 'top', + available: this.canAccessPersonalSettings(), + activateOnRouteNames: [ VIEWS.PERSONAL_SETTINGS ], + }, + { + id: 'settings-users', + icon: 'user-friends', + label: this.$locale.baseText('settings.users'), + position: 'top', + available: this.canAccessUsersSettings(), + activateOnRouteNames: [ VIEWS.USERS_SETTINGS ], + }, + { + id: 'settings-api', + icon: 'plug', + label: this.$locale.baseText('settings.n8napi'), + position: 'top', + available: this.canAccessApiSettings(), + activateOnRouteNames: [ VIEWS.API_SETTINGS ], + }, + ]; + + for (const item of this.settingsFakeDoorFeatures) { + if (item.uiLocations.includes('settings')) { + menuItems.push({ + id: item.id, + icon: item.icon || 'question', + label: this.$locale.baseText(item.featureName as BaseTextKey), + position: 'top', + available: true, + activateOnRoutePaths: [ `/settings/coming-soon/${item.id}` ], + }); + } + } + + menuItems.push( + { + id: 'settings-community-nodes', + icon: 'cube', + label: this.$locale.baseText('settings.communityNodes'), + position: 'top', + available: this.canAccessCommunityNodes(), + activateOnRouteNames: [ VIEWS.COMMUNITY_NODES ], + }, + ); + + return menuItems; + }, + }, + mounted() { + this.pushConnect(); }, methods: { canAccessPersonalSettings(): boolean { @@ -96,83 +125,60 @@ export default mixins( openUpdatesPanel() { this.$store.dispatch('ui/openModal', VERSIONS_MODAL_KEY); }, - }, - mounted() { - this.pushConnect(); + async handleSelect (key: string) { + switch (key) { + case 'settings-personal': + if (this.$router.currentRoute.name !== VIEWS.PERSONAL_SETTINGS) { + this.$router.push({ name: VIEWS.PERSONAL_SETTINGS }); + } + break; + case 'settings-users': + if (this.$router.currentRoute.name !== VIEWS.USERS_SETTINGS) { + this.$router.push({ name: VIEWS.USERS_SETTINGS }); + } + break; + case 'settings-api': + if (this.$router.currentRoute.name !== VIEWS.API_SETTINGS) { + this.$router.push({ name: VIEWS.API_SETTINGS }); + } + break; + case 'environments': + case 'logging': + this.$router.push({ name: VIEWS.FAKE_DOOR, params: { featureId: key } }).catch(() => {}); + break; + case 'settings-community-nodes': + if (this.$router.currentRoute.name !== VIEWS.COMMUNITY_NODES) { + this.$router.push({ name: VIEWS.COMMUNITY_NODES }); + } + break; + default: + break; + } + }, }, });