mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 21:37:32 -08:00
feat(editor): updated n8n-menu component (#4290)
* refactor(editor): N8N-4540 Main navigation layout rework (#4060) * ✨ Implemented new editor layout using css grid * ✨ Reworking main navigation layout, migrating some styling to css modules * ✨ Reworking main sidebar layout and responsiveness * 💄 Minor type update * ✨ Updated editor grid layout so empty cells are collapsed (`fit-content`), fixed updates menu items styling * ✨ Implemented new user area look & feel in main sidebar * 💄 Adjusting sidebar bottom padding when user area is not shown * 💄 CSS cleanup/refactor + minor vue refactoring * ✨ Fixing overscoll issue in chrome and scrolling behaviour of the content view * 👌 Addressing review feedback * ✨ Added collapsed and expanded versions of n8n logo * ✨ Updating infinite scrolling in templates view to work with the new layout * 💄 Updating main sidebar expanded width and templates view left margin * 💄 Updating main content height * 💄 Adding global styles for scrollable views with centered content, minor updates to user area * ✨ Updating zoomToFit logic, lasso select box position and new nodes positioning * ✨ Fixing new node drop position now that mouse detection has been adjusted * 👌 Updating templates view scroll to top logic and responsive padding, aligning menu items titles * 💄 Moving template layout style from global css class to component level * ✨ Moved 'Workflows' menu to node view header. Added new dropdown component for user area and the new WF menu * 💄 Updating disabled states in new WF menu * 💄 Initial stab at new sidebar styling * ✨ Finished main navigation restyling * ✨ Updating `zoomToFit` and centering logic * ✨ Adding updates menu item to settings sidebar * 💄 Adding updates item to the settings sidebar and final touches on main sidebar style * 💄 Removing old code & refactoring * 💄 Minor CSS tweaks * 💄 Opening credentials modal on sidebar menu item click. Minor CSS updates * 💄 Updating sidebar expand/collapse animation * 💄 Few more refinements of sidebar animation * 👌 Addressing code review comments * ✨ Moved ActionDropdown component to design system * 👌 Fixing bugs reported during code review and testing * 👌 Addressing design review comments for the new sidebar * ✔️ Updating `N8nActionDropdown` component tests * ✨ Remembering scroll position when going back to templates list * ✨ Updating zoomToFit logic to account for footer content * 👌 Addressing latest sidebar review comments * ✨ New `n8n-menu-item` component * ✨ Implemented new `n8n-menu` design system component, updated menu items to support collapsed mode * Minor update to n8n-menu storybook entry * 💄 Updating collapsed sub-menu style. Fixing vue error on hover. * ⚡ Changing IMenuItem from interface to type * ✨ Added new n8n-menu component to editor main sidebar * ⚡ Finished main sidebar * ⚡ Added new menus to setttings and credentials view * ✨ Implemented tab and router modes for n8n-menu * ✨ Implemented credentials menus using new n8n-menu component * 💄 Finishing main and settings sidebar details, updating design system stories * 💄 Adding injected items support to main sidebar, handling navigation errors, small tweaks * ✔️ Fixing linting errors * ✔️ Addressing typescript errors in design system components * ⚡ Using design-system types in editor UI * 💄 Add support for custom icon size in menu items * 👌 Addressing code review and design review feedback * 💄 Minor updates
This commit is contained in:
parent
d47ff48fb6
commit
6af3ba75dc
|
@ -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:
|
||||
`<n8n-menu v-bind="$props" @select="onSelect">
|
||||
<n8n-menu-item index="1"> <span slot="title">Item 1</span> </n8n-menu-item>
|
||||
<n8n-menu-item index="2"> <span slot="title">Item 2</span> </n8n-menu-item>
|
||||
</n8n-menu>`,
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.parameters = {
|
||||
backgrounds: { default: '--color-background-light' },
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
type: 'secondary',
|
||||
};
|
160
packages/design-system/src/components/N8nMenu/Menu.stories.ts
Normal file
160
packages/design-system/src/components/N8nMenu/Menu.stories.ts
Normal file
|
@ -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: `
|
||||
<div style="height: 90vh; width: 200px">
|
||||
<n8n-menu v-bind="$props" @select="onSelect"></n8n-menu>
|
||||
</div>
|
||||
`,
|
||||
methods,
|
||||
});
|
||||
|
||||
const templateWithHeaderAndFooter: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nMenu,
|
||||
N8nIcon,
|
||||
N8nText,
|
||||
},
|
||||
template: `
|
||||
<div style="height: 90vh; width: 200px">
|
||||
<n8n-menu v-bind="$props" @select="onSelect">
|
||||
<template #header>
|
||||
<a href="#" class="p-m hideme" style="display: block;">
|
||||
<n8n-icon icon="long-arrow-alt-left"/> Back to home
|
||||
</a>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="p-m hideme">
|
||||
<n8n-icon icon="user-circle" size="xlarge"/>
|
||||
<n8n-text>John Smithson</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
`,
|
||||
methods,
|
||||
});
|
||||
|
||||
const templateWithAllSlots: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nMenu,
|
||||
N8nIcon,
|
||||
N8nText,
|
||||
},
|
||||
template: `
|
||||
<div style="height: 90vh; width: 200px">
|
||||
<n8n-menu v-bind="$props" @select="onSelect">
|
||||
<template #header>
|
||||
<a href="#" class="p-m hideme" style="display: block;">
|
||||
<n8n-icon icon="long-arrow-alt-left"/> Back to home
|
||||
</a>
|
||||
</template>
|
||||
<template #menuPrefix>
|
||||
<n8n-text class="hideme" size="small" color="text-light">Something can be added here...</n8n-text>
|
||||
</template>
|
||||
<template #menuSuffix>
|
||||
<n8n-text class="hideme" size="small" color="text-light">...and here if needed</n8n-text>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="p-m hideme">
|
||||
<n8n-icon icon="user-circle" size="xlarge"/>
|
||||
<n8n-text>John Smithson</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
`,
|
||||
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 };
|
|
@ -1,22 +1,83 @@
|
|||
<template>
|
||||
<el-menu
|
||||
:defaultActive="defaultActive"
|
||||
:collapse="collapse"
|
||||
:router="router"
|
||||
:class="['n8n-menu', $style[type + (light ? '-light' : '')]]"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-menu>
|
||||
<div :class="{
|
||||
['menu-container']: true,
|
||||
[$style.container]: true,
|
||||
[$style.menuCollapsed]: collapsed
|
||||
}">
|
||||
<div v-if="$slots.header" :class="$style.menuHeader">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div :class="$style.menuContent">
|
||||
<div :class="{[$style.upperContent]: true, ['pt-xs']: $slots.menuPrefix }">
|
||||
<div v-if="$slots.menuPrefix" :class="$style.menuPrefix">
|
||||
<slot name="menuPrefix"></slot>
|
||||
</div>
|
||||
<el-menu
|
||||
:defaultActive="defaultActive"
|
||||
:collapse="collapsed"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<n8n-menu-item
|
||||
v-for="item in upperMenuItems"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:compact="collapsed"
|
||||
:popperClass="$style.submenuPopper"
|
||||
:tooltipDelay="tooltipDelay"
|
||||
:mode="mode"
|
||||
:activeTab="activeTab"
|
||||
@click="onSelect"
|
||||
/>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div :class="{[$style.lowerContent]: true, ['pb-xs']: $slots.menuSuffix }">
|
||||
<el-menu
|
||||
:defaultActive="defaultActive"
|
||||
:collapse="collapsed"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<n8n-menu-item
|
||||
v-for="item in lowerMenuItems"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:compact="collapsed"
|
||||
:popperClass="$style.submenuPopper"
|
||||
:tooltipDelay="tooltipDelay"
|
||||
:mode="mode"
|
||||
:activeTab="activeTab"
|
||||
@click="onSelect"
|
||||
/>
|
||||
</el-menu>
|
||||
<div v-if="$slots.menuSuffix" :class="$style.menuSuffix">
|
||||
<slot name="menuSuffix"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.footer" :class="$style.menuFooter">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
import N8nMenuItem from '../N8nMenuItem';
|
||||
|
||||
import Vue from 'vue';
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { Route } from 'vue-router';
|
||||
import { IMenuItem } from '../../types';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'n8n-menu',
|
||||
components: {
|
||||
ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
N8nMenuItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: '',
|
||||
};
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
@ -26,55 +87,96 @@ export default Vue.extend({
|
|||
defaultActive: {
|
||||
type: String,
|
||||
},
|
||||
collapse: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
light: {
|
||||
type: Boolean,
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'router',
|
||||
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
|
||||
},
|
||||
router: {
|
||||
type: Boolean,
|
||||
tooltipDelay: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<IMenuItem[]>,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ElMenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
mounted() {
|
||||
if (this.mode === 'router') {
|
||||
const found = this.items.find(item => {
|
||||
return Array.isArray(item.activateOnRouteNames) && item.activateOnRouteNames.includes(this.$route.name || '') ||
|
||||
Array.isArray(item.activateOnRoutePaths) && item.activateOnRoutePaths.includes(this.$route.path);
|
||||
});
|
||||
this.activeTab = found ? found.id : '';
|
||||
} else {
|
||||
this.activeTab = this.items.length > 0 ? this.items[0].id : '';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
upperMenuItems(): IMenuItem[] {
|
||||
return this.items.filter((item: IMenuItem) => item.position === 'top' && item.available !== false);
|
||||
},
|
||||
lowerMenuItems(): IMenuItem[] {
|
||||
return this.items.filter((item: IMenuItem) => item.position === 'bottom' && item.available !== false);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSelect(event: MouseEvent, option: string): void {
|
||||
if (this.mode === 'tabs') {
|
||||
this.activeTab = option;
|
||||
}
|
||||
this.$emit('select', option);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.menu {
|
||||
max-width: 200px;
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-background-xlight);
|
||||
}
|
||||
|
||||
.primary {
|
||||
composes: menu;
|
||||
--menu-item-hover-font-color: var(--color-primary);
|
||||
}
|
||||
.menuContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
|
||||
.secondary {
|
||||
composes: menu;
|
||||
--menu-font-color: var(--color-text-base);
|
||||
--menu-item-font-weight: var(--font-weight-regular);
|
||||
--menu-background-color: transparent;
|
||||
--menu-item-active-font-color: var(--color-text-dark);
|
||||
--menu-item-active-background-color: var(--color-foreground-base);
|
||||
--menu-item-hover-font-color: var(--color-primary);
|
||||
--menu-item-border-radius: 4px;
|
||||
--menu-item-height: 38px;
|
||||
|
||||
li {
|
||||
padding-left: 12px !important;
|
||||
& > div > :global(.el-menu) {
|
||||
background: none;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-light {
|
||||
composes: secondary;
|
||||
--menu-item-active-background-color: hsl(
|
||||
var(--color-foreground-base-h),
|
||||
var(--color-foreground-base-s),
|
||||
var(--color-foreground-base-l),
|
||||
0.7
|
||||
);
|
||||
.upperContent {
|
||||
ul {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.submenuPopper {
|
||||
bottom: auto !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lowerContent {
|
||||
ul {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menuCollapsed {
|
||||
transition: width 150ms ease-in-out;
|
||||
:global(.hideme) { display: none !important; }
|
||||
}
|
||||
|
||||
.menuPrefix, .menuSuffix {
|
||||
padding: var(--spacing-xs) var(--spacing-l);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -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: `
|
||||
<div style="width: 200px">
|
||||
<el-menu>
|
||||
<n8n-menu-item v-bind="$props" />
|
||||
</el-menu>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -1,7 +1,272 @@
|
|||
<template>
|
||||
<el-submenu
|
||||
v-if="item.children && item.children.length > 0"
|
||||
:id="item.id"
|
||||
:class="{
|
||||
[$style.submenu]: true,
|
||||
[$style.item]: true,
|
||||
[$style.compact]: compact,
|
||||
[$style.active]: mode === 'router' && isItemActive(item)
|
||||
}"
|
||||
:index="item.id"
|
||||
:popper-append-to-body="false"
|
||||
:popper-class="`${$style.submenuPopper} ${popperClass}`"
|
||||
>
|
||||
<template slot="title">
|
||||
<n8n-icon v-if="item.icon" :class="$style.icon" :icon="item.icon" :size="item.customIconSize || 'large'" />
|
||||
<span :class="$style.label">{{ item.label }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in availableChildren"
|
||||
:key="child.id"
|
||||
:id="child.id"
|
||||
:class="{
|
||||
[$style.menuItem]: true,
|
||||
[$style.disableActiveStyle]: !isItemActive(child),
|
||||
[$style.active]: isItemActive(child),
|
||||
}"
|
||||
:index="child.id"
|
||||
@click="onItemClick(child)"
|
||||
>
|
||||
<n8n-icon v-if="child.icon" :class="$style.icon" :icon="child.icon" />
|
||||
<span :class="$style.label">{{ child.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
<n8n-tooltip v-else placement="right" :content="item.label" :disabled="!compact" :open-delay="tooltipDelay">
|
||||
<el-menu-item
|
||||
:id="item.id"
|
||||
:class="{
|
||||
[$style.menuItem]: true,
|
||||
[$style.item]: true,
|
||||
[$style.disableActiveStyle]: !isItemActive(item),
|
||||
[$style.active]: isItemActive(item),
|
||||
[$style.compact]: compact
|
||||
}"
|
||||
:index="item.id"
|
||||
@click="onItemClick(item)"
|
||||
>
|
||||
<n8n-icon v-if="item.icon" :class="$style.icon" :icon="item.icon" :size="item.customIconSize || 'large'" />
|
||||
<span :class="$style.label">{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</n8n-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ElSubmenu from 'element-ui/lib/submenu';
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import { IMenuItem } from '../../types';
|
||||
import Vue from 'vue';
|
||||
import { Route } from 'vue-router';
|
||||
|
||||
ElMenuItem.name = 'n8n-menu-item'; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
export default Vue.extend({
|
||||
name: 'n8n-menu-item',
|
||||
components: {
|
||||
ElSubmenu, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
ElMenuItem, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
||||
N8nIcon,
|
||||
N8nTooltip,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object as () => IMenuItem,
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tooltipDelay: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'router',
|
||||
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
|
||||
},
|
||||
activeTab: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
availableChildren(): IMenuItem[] {
|
||||
return Array.isArray(this.item.children) ? this.item.children.filter(child => child.available !== false) : [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isItemActive(item: IMenuItem): boolean {
|
||||
const isItemActive = this.isActive(item);
|
||||
const hasActiveChild = Array.isArray(item.children) && item.children.some(child => this.isActive(child));
|
||||
return isItemActive || hasActiveChild;
|
||||
},
|
||||
isActive(item: IMenuItem): boolean {
|
||||
if (this.mode === 'router') {
|
||||
if (item.activateOnRoutePaths) {
|
||||
return Array.isArray(item.activateOnRoutePaths) && item.activateOnRoutePaths.includes(this.$route.path);
|
||||
} else if (item.activateOnRouteNames) {
|
||||
return Array.isArray(item.activateOnRouteNames) && item.activateOnRouteNames.includes(this.$route.name || '');
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return item.id === this.activeTab;
|
||||
}
|
||||
},
|
||||
onItemClick(item: IMenuItem, event: MouseEvent) {
|
||||
if (item && item.type === 'link' && item.properties) {
|
||||
const href: string = item.properties.href;
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.properties.newWindow) {
|
||||
window.open(href);
|
||||
}
|
||||
else {
|
||||
window.location.assign(item.properties.href);
|
||||
}
|
||||
|
||||
}
|
||||
this.$emit('click', event, item.id);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default ElMenuItem;
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
// Element menu-item overrides
|
||||
:global(.el-menu-item), :global(.el-submenu__title) {
|
||||
--menu-font-color: var(--color-text-base);
|
||||
--menu-item-active-background-color: var(--color-foreground-base);
|
||||
--menu-item-active-font-color: var(--color-text-dark);
|
||||
--menu-item-hover-fill: var(--color-foreground-base);
|
||||
--menu-item-hover-font-color: var(--color-text-dark);
|
||||
--menu-item-height: 35px;
|
||||
--submenu-item-height: 27px;
|
||||
}
|
||||
|
||||
|
||||
.submenu {
|
||||
:global(.el-submenu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: var(--spacing-2xs) var(--spacing-xs) !important;
|
||||
user-select: none;
|
||||
|
||||
i {
|
||||
padding-top: 2px;
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon { color: var(--color-text-dark) }
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
height: var(--submenu-item-height) !important;
|
||||
min-width: auto !important;
|
||||
margin: var(--spacing-2xs) 0 !important;
|
||||
padding-left: var(--spacing-l) !important;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
.icon { color: var(--color-text-dark) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
.disableActiveStyle {
|
||||
background-color: initial !important;
|
||||
color: var(--color-text-base) !important;
|
||||
|
||||
svg {
|
||||
color: var(--color-text-base) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-foreground-base) !important;
|
||||
svg {
|
||||
color: var(--color-text-dark) !important;
|
||||
}
|
||||
&:global(.el-submenu) {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
.icon { color: var(--color-text-dark) }
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
display: flex;
|
||||
padding: var(--spacing-2xs) var(--spacing-xs) !important;
|
||||
margin: 0 !important;
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
min-width: var(--spacing-s);
|
||||
margin-right: var(--spacing-xs);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.compact {
|
||||
width: 40px;
|
||||
.icon {
|
||||
margin: 0;
|
||||
overflow: visible !important;
|
||||
visibility: visible !important;
|
||||
width: initial !important;
|
||||
height: initial !important;
|
||||
}
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.submenuPopper {
|
||||
display: block;
|
||||
left: 40px !important;
|
||||
bottom: 110px !important;
|
||||
top: auto !important;
|
||||
|
||||
ul {
|
||||
padding: 0 var(--spacing-xs) !important;
|
||||
}
|
||||
.menuItem {
|
||||
display: flex;
|
||||
padding: var(--spacing-2xs) var(--spacing-xs) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './form';
|
||||
export * from './user';
|
||||
export * from './menu';
|
||||
export * from './button';
|
||||
|
|
21
packages/design-system/src/types/menu.ts
Normal file
21
packages/design-system/src/types/menu.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -49,34 +49,7 @@
|
|||
<template slot="content">
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.sidebar">
|
||||
<n8n-menu
|
||||
type="secondary"
|
||||
@select="onTabSelect"
|
||||
defaultActive="connection"
|
||||
:light="true"
|
||||
>
|
||||
<n8n-menu-item index="connection">
|
||||
<span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.connection') }}</span>
|
||||
</n8n-menu-item>
|
||||
<enterprise-edition v-if="credentialType" :features="[EnterpriseEditionFeature.Sharing]">
|
||||
<n8n-menu-item index="sharing">
|
||||
<span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.sharing') }}</span>
|
||||
</n8n-menu-item>
|
||||
<template #fallback>
|
||||
<n8n-menu-item
|
||||
v-for="fakeDoor in credentialsFakeDoorFeatures"
|
||||
v-bind:key="fakeDoor.featureName"
|
||||
:index="`coming-soon/${fakeDoor.id}`"
|
||||
:class="$style.tab"
|
||||
>
|
||||
<span slot="title">{{ $locale.baseText(fakeDoor.featureName) }}</span>
|
||||
</n8n-menu-item>
|
||||
</template>
|
||||
</enterprise-edition>
|
||||
<n8n-menu-item v-if="credentialType" index="details">
|
||||
<span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.details') }}</span>
|
||||
</n8n-menu-item>
|
||||
</n8n-menu>
|
||||
<n8n-menu mode="tabs" :items="sidebarItems" @select="onTabSelect" ></n8n-menu>
|
||||
</div>
|
||||
<div v-if="activeTab === 'connection'" :class="$style.mainContent" ref="content">
|
||||
<CredentialConfig
|
||||
|
@ -170,6 +143,8 @@ import {IDataObject} from "n8n-workflow";
|
|||
import FeatureComingSoon from '../FeatureComingSoon.vue';
|
||||
import {mapGetters} from "vuex";
|
||||
import {getCredentialPermissions, IPermissions} from "@/permissions";
|
||||
import { IMenuItem } from 'n8n-design-system';
|
||||
import { BaseTextKey } from '@/plugins/i18n';
|
||||
|
||||
interface NodeAccessMap {
|
||||
[nodeType: string]: ICredentialNodeAccess | null;
|
||||
|
@ -421,6 +396,43 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
|
||||
return getCredentialPermissions(this.currentUser, (this.credentialId ? this.currentCredential : this.credentialData) as ICredentialsResponse, this.$store);
|
||||
},
|
||||
sidebarItems(): IMenuItem[] {
|
||||
const items: IMenuItem[] = [
|
||||
{
|
||||
id: 'connection',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.connection'),
|
||||
position: 'top',
|
||||
},
|
||||
{
|
||||
id: 'sharing',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.sharing'),
|
||||
position: 'top',
|
||||
available: this.credentialType !== null && this.isSharingAvailable,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.credentialType !== null && !this.isSharingAvailable) {
|
||||
for (const item of this.credentialsFakeDoorFeatures) {
|
||||
items.push({
|
||||
id: `coming-soon/${item.id}`,
|
||||
label: this.$locale.baseText(item.featureName as BaseTextKey),
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: 'details',
|
||||
label: this.$locale.baseText('credentialEdit.credentialEdit.details'),
|
||||
position: 'top',
|
||||
},
|
||||
);
|
||||
return items;
|
||||
},
|
||||
isSharingAvailable(): boolean {
|
||||
return this.$store.getters['settings/isEnterpriseFeatureEnabled'](EnterpriseEditionFeature.Sharing) === true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async beforeClose() {
|
||||
|
@ -458,7 +470,6 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
|
||||
return false;
|
||||
},
|
||||
|
||||
displayCredentialParameter(parameter: INodeProperties): boolean {
|
||||
if (parameter.type === 'hidden') {
|
||||
return false;
|
||||
|
@ -979,6 +990,10 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
min-width: 170px;
|
||||
margin-right: var(--spacing-l);
|
||||
flex-grow: 1;
|
||||
|
||||
ul {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
|
@ -4,132 +4,51 @@
|
|||
[$style.sideMenu]: true,
|
||||
[$style.sideMenuCollapsed]: isCollapsed
|
||||
}">
|
||||
<div :class="{[$style.sideMenuWrapper]: true, [$style.expanded]: !isCollapsed}">
|
||||
<div
|
||||
id="collapse-change-button"
|
||||
:class="{
|
||||
['clickable']: true,
|
||||
[$style.sideMenuCollapseButton]: true,
|
||||
[$style.expandedButton]: !isCollapsed
|
||||
}"
|
||||
@click="toggleCollapse"
|
||||
></div>
|
||||
<n8n-menu :default-active="$route.path" @select="handleSelect" :collapse="isCollapsed">
|
||||
<n8n-menu-item
|
||||
index="logo"
|
||||
:class="[$style.logoItem, $style.disableActiveStyle]"
|
||||
>
|
||||
<div
|
||||
id="collapse-change-button"
|
||||
:class="{ ['clickable']: true, [$style.sideMenuCollapseButton]: true, [$style.expandedButton]: !isCollapsed }"
|
||||
@click="toggleCollapse">
|
||||
</div>
|
||||
<n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect">
|
||||
<template #header>
|
||||
<div :class="$style.logo">
|
||||
<img :src="basePath + (isCollapsed ? 'n8n-logo-collapsed.svg' : 'n8n-logo-expanded.svg')" :class="$style.icon" alt="n8n"/>
|
||||
</n8n-menu-item>
|
||||
<div :class="$style.sideMenuFlexContainer">
|
||||
<div :class="$style.sideMenuUpper">
|
||||
|
||||
<MenuItemsIterator :items="sidebarMenuTopItems" :root="true"/>
|
||||
|
||||
<el-submenu
|
||||
index="/workflow"
|
||||
title="Workflow"
|
||||
popperClass="sidebar-popper"
|
||||
:class="{
|
||||
[$style.workflowSubmenu]: true,
|
||||
[$style.active]: $route.path === '/workflow'
|
||||
}"
|
||||
>
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="network-wired"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.workflows') }}</span>
|
||||
</template>
|
||||
|
||||
<n8n-menu-item index="/workflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-open" :class="$style.disableActiveStyle">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<n8n-menu-item v-if="isTemplatesEnabled" index="/templates" :class="$style.templatesSubmenu">
|
||||
<font-awesome-icon icon="box-open"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.templates') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<n8n-menu-item index="/credentials" :class="$style.credentialsSubmenu">
|
||||
<font-awesome-icon icon="key"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.credentials') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<n8n-menu-item index="executions" :class="[$style.disableActiveStyle, $style.executionsSubmenu]">
|
||||
<font-awesome-icon icon="tasks"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.executions') }}</span>
|
||||
</n8n-menu-item>
|
||||
</div>
|
||||
<div :class="$style.sideMenuLower">
|
||||
<n8n-menu-item index="settings" v-if="canUserAccessSettings && currentUser" :class="$style.settingsSubmenu">
|
||||
<font-awesome-icon icon="cog"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('settings') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<el-submenu index="help" :class="[$style.helpMenu, $style.disableActiveStyle]" title="Help" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="question"/>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.help') }}</span>
|
||||
</template>
|
||||
|
||||
<MenuItemsIterator :items="helpMenuItems" :afterItemClick="trackHelpItemClick" />
|
||||
|
||||
<n8n-menu-item index="help-about">
|
||||
<template slot="title">
|
||||
<font-awesome-icon :class="$style['about-icon']" icon="info"/>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.aboutN8n') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<MenuItemsIterator :items="sidebarMenuBottomItems" :root="true"/>
|
||||
|
||||
<div :class="{
|
||||
[$style.footerMenuItems] : true,
|
||||
[$style.loggedIn]: showUserArea,
|
||||
}">
|
||||
<n8n-menu-item index="updates" :class="$style.updatesSubmenu" v-if="hasVersionUpdates" @click="openUpdatesPanel">
|
||||
<div :class="$style.giftContainer">
|
||||
<GiftNotificationIcon />
|
||||
</div>
|
||||
<span slot="title" :class="['item-title-root', $style.updatesLabel]">
|
||||
{{nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : ''}}
|
||||
</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item v-if="showUserArea" :class="$style.userSubmenu" index="">
|
||||
<!-- This dropdown is only enabled when sidebar is collapsed -->
|
||||
<el-dropdown :disabled="!isCollapsed" placement="right-end" trigger="click" @command="onUserActionToggle">
|
||||
<div :class="{[$style.avatar]: true, ['clickable']: isCollapsed }">
|
||||
<n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="small" />
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="settings">{{ $locale.baseText('settings') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">{{ $locale.baseText('auth.signout') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</el-dropdown>
|
||||
<div slot="title" :class="['item-title-root', $style.username ]" v-if="!isCollapsed">
|
||||
<span :title="currentUser.fullName" :class="$style.fullName">{{currentUser.fullName}}</span>
|
||||
<div :class="{[$style.userActions]: true, ['user-actions']: true }">
|
||||
<n8n-action-dropdown :items="userMenuItems" placement="top-start" @select="onUserActionToggle" />
|
||||
</div>
|
||||
</div>
|
||||
</n8n-menu-item>
|
||||
</div>
|
||||
</template>
|
||||
<template #menuSuffix v-if="hasVersionUpdates">
|
||||
<div :class="$style.updates" @click="openUpdatesPanel">
|
||||
<div :class="$style.giftContainer">
|
||||
<GiftNotificationIcon />
|
||||
</div>
|
||||
<n8n-text :class="{['ml-xs']: true, [$style.expanded]: fullyExpanded }" color="text-base">
|
||||
{{ nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : '' }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer v-if="showUserArea">
|
||||
<div :class="$style.userArea">
|
||||
<div class="ml-3xs">
|
||||
<!-- This dropdown is only enabled when sidebar is collapsed -->
|
||||
<el-dropdown :disabled="!isCollapsed" placement="right-end" trigger="click" @command="onUserActionToggle">
|
||||
<div :class="{[$style.avatar]: true, ['clickable']: isCollapsed }">
|
||||
<n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="small" />
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="settings">{{ $locale.baseText('settings') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">{{ $locale.baseText('auth.signout') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div :class="{ ['ml-2xs']: true, [$style.userName]: true, [$style.expanded]: fullyExpanded }">
|
||||
<n8n-text size="small" :bold="true" color="text-dark">{{currentUser.fullName}}</n8n-text>
|
||||
</div>
|
||||
<div :class="{ [$style.userActions]: true, [$style.expanded]: fullyExpanded }">
|
||||
<n8n-action-dropdown :items="userMenuItems" placement="top-start" @select="onUserActionToggle" />
|
||||
</div>
|
||||
</div>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -151,7 +70,6 @@ import { workflowRun } from '@/components/mixins/workflowRun';
|
|||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuItemsIterator from './MenuItemsIterator.vue';
|
||||
import {
|
||||
MODAL_CANCEL,
|
||||
MODAL_CLOSE,
|
||||
|
@ -181,22 +99,12 @@ export default mixins(
|
|||
ExecutionsList,
|
||||
GiftNotificationIcon,
|
||||
WorkflowSettings,
|
||||
MenuItemsIterator,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// @ts-ignore
|
||||
basePath: this.$store.getters.getBaseUrl,
|
||||
userMenuItems: [
|
||||
{
|
||||
id: 'settings',
|
||||
label: this.$locale.baseText('settings'),
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
label: this.$locale.baseText('auth.signout'),
|
||||
},
|
||||
],
|
||||
fullyExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -223,61 +131,154 @@ export default mixins(
|
|||
showUserArea(): boolean {
|
||||
return this.isUserManagementEnabled && this.canUserAccessSidebarUserInfo && this.currentUser;
|
||||
},
|
||||
helpMenuItems (): object[] {
|
||||
return [
|
||||
{
|
||||
id: 'quickstart',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
|
||||
icon: 'video',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
|
||||
icon: 'book',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
|
||||
icon: 'users',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io/courses',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
|
||||
icon: 'graduation-cap',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
workflowExecution (): IExecutionResponse | null {
|
||||
return this.$store.getters.getWorkflowExecution;
|
||||
},
|
||||
sidebarMenuTopItems(): IMenuItem[] {
|
||||
return this.$store.getters.sidebarMenuItems.filter((item: IMenuItem) => item.position === 'top');
|
||||
userMenuItems (): object[] {
|
||||
return [
|
||||
{
|
||||
id: 'settings',
|
||||
label: this.$locale.baseText('settings'),
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
label: this.$locale.baseText('auth.signout'),
|
||||
},
|
||||
];
|
||||
},
|
||||
sidebarMenuBottomItems(): IMenuItem[] {
|
||||
return this.$store.getters.sidebarMenuItems.filter((item: IMenuItem) => item.position === 'bottom');
|
||||
mainMenuItems (): IMenuItem[] {
|
||||
const items: IMenuItem[] = [];
|
||||
const injectedItems = this.$store.getters.sidebarMenuItems as IMenuItem[];
|
||||
|
||||
if (injectedItems && injectedItems.length > 0) {
|
||||
for(const item of injectedItems) {
|
||||
items.push(
|
||||
{
|
||||
id: item.id,
|
||||
// @ts-ignore
|
||||
icon: item.properties ? item.properties.icon : '',
|
||||
// @ts-ignore
|
||||
label: item.properties ? item.properties.title : '',
|
||||
position: item.position,
|
||||
activateOnRouteNames: [ VIEWS.TEMPLATES ],
|
||||
type: item.properties?.href ? 'link' : 'regular',
|
||||
properties: item.properties,
|
||||
} as IMenuItem,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const regularItems: IMenuItem[] = [
|
||||
{
|
||||
id: 'workflows',
|
||||
icon: 'network-wired',
|
||||
label: this.$locale.baseText('mainSidebar.workflows'),
|
||||
position: 'top',
|
||||
activateOnRouteNames: [ VIEWS.NEW_WORKFLOW, VIEWS.WORKFLOWS, VIEWS.WORKFLOW ],
|
||||
children: [
|
||||
{
|
||||
id: 'workflow',
|
||||
label: this.$locale.baseText('mainSidebar.new'),
|
||||
icon: 'file',
|
||||
activateOnRouteNames: [ VIEWS.NEW_WORKFLOW ],
|
||||
},
|
||||
{
|
||||
id: 'workflow-open',
|
||||
label: this.$locale.baseText('mainSidebar.open'),
|
||||
icon: 'folder-open',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'templates',
|
||||
icon: 'box-open',
|
||||
label: this.$locale.baseText('mainSidebar.templates'),
|
||||
position: 'top',
|
||||
available: this.isTemplatesEnabled,
|
||||
activateOnRouteNames: [ VIEWS.TEMPLATES ],
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
icon: 'key',
|
||||
label: this.$locale.baseText('mainSidebar.credentials'),
|
||||
customIconSize: 'medium',
|
||||
position: 'top',
|
||||
activateOnRouteNames: [ VIEWS.CREDENTIALS ],
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
icon: 'tasks',
|
||||
label: this.$locale.baseText('mainSidebar.executions'),
|
||||
position: 'top',
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
icon: 'cog',
|
||||
label: this.$locale.baseText('settings'),
|
||||
position: 'bottom',
|
||||
available: this.canUserAccessSettings && this.currentUser,
|
||||
activateOnRouteNames: [ VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS ],
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
icon: 'question',
|
||||
label: 'Help',
|
||||
position: 'bottom',
|
||||
children: [
|
||||
{
|
||||
id: 'quickstart',
|
||||
icon: 'video',
|
||||
label: this.$locale.baseText('mainSidebar.helpMenuItems.quickstart'),
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
icon: 'book',
|
||||
label: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
icon: 'users',
|
||||
label: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
icon: 'graduation-cap',
|
||||
label: this.$locale.baseText('mainSidebar.helpMenuItems.course'),
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://www.youtube.com/watch?v=RpjQTGKm-ok',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
icon: 'info',
|
||||
label: this.$locale.baseText('mainSidebar.aboutN8n'),
|
||||
position: 'bottom',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return [ ...items, ...regularItems ];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fullyExpanded = !this.isCollapsed;
|
||||
if (this.$refs.user) {
|
||||
this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user });
|
||||
}
|
||||
|
@ -320,13 +321,21 @@ export default mixins(
|
|||
},
|
||||
toggleCollapse () {
|
||||
this.$store.commit('ui/toggleSidebarMenuCollapse');
|
||||
// When expanding, delay showing some element to ensure smooth animation
|
||||
if (!this.isCollapsed) {
|
||||
setTimeout(() => {
|
||||
this.fullyExpanded = !this.isCollapsed;
|
||||
}, 300);
|
||||
} else {
|
||||
this.fullyExpanded = !this.isCollapsed;
|
||||
}
|
||||
},
|
||||
openUpdatesPanel() {
|
||||
this.$store.dispatch('ui/openModal', VERSIONS_MODAL_KEY);
|
||||
},
|
||||
async handleSelect (key: string) {
|
||||
switch (key) {
|
||||
case '/workflow': {
|
||||
case 'workflow': {
|
||||
await this.createNewWorkflow();
|
||||
break;
|
||||
}
|
||||
|
@ -334,14 +343,16 @@ export default mixins(
|
|||
this.$store.dispatch('ui/openModal', WORKFLOW_OPEN_MODAL_KEY);
|
||||
break;
|
||||
}
|
||||
case '/templates': {
|
||||
case 'templates': {
|
||||
if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
|
||||
this.$router.push({ name: VIEWS.TEMPLATES });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '/credentials': {
|
||||
this.$router.push({name: VIEWS.CREDENTIALS});
|
||||
case 'credentials': {
|
||||
if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
|
||||
this.$router.push({name: VIEWS.CREDENTIALS});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'executions': {
|
||||
|
@ -352,15 +363,24 @@ export default mixins(
|
|||
const defaultRoute = this.findFirstAccessibleSettingsRoute();
|
||||
if (defaultRoute) {
|
||||
const routeProps = this.$router.resolve({ name: defaultRoute });
|
||||
this.$router.push(routeProps.route.path);
|
||||
if (this.$router.currentRoute.name !== defaultRoute) {
|
||||
this.$router.push(routeProps.route.path);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'help-about': {
|
||||
case 'about': {
|
||||
this.trackHelpItemClick('about');
|
||||
this.$store.dispatch('ui/openModal', ABOUT_MODAL_KEY);
|
||||
break;
|
||||
}
|
||||
case 'quickstart':
|
||||
case 'docs':
|
||||
case 'forum':
|
||||
case 'examples' : {
|
||||
this.trackHelpItemClick(key);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
},
|
||||
|
@ -444,162 +464,34 @@ export default mixins(
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#side-menu {
|
||||
.el-menu {
|
||||
--menu-item-active-background-color: var(--color-foreground-base);
|
||||
--menu-item-active-font-color: var(--color-text-dark);
|
||||
--menu-item-hover-fill: var(--color-foreground-base);
|
||||
--menu-item-hover-font-color: var(--color-text-dark);
|
||||
--menu-item-height: 35px;
|
||||
--submenu-item-height: 27px;
|
||||
|
||||
.el-icon-arrow-down {
|
||||
font-weight: bold;
|
||||
right: 12px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item:hover, .el-submenu__title:hover, .el-menu-item.is-active {
|
||||
svg {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item, .el-menu-item .el-tooltip, .el-submenu__title {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
.el-menu-item, .el-submenu__title {
|
||||
margin: 8px 0;
|
||||
border-radius: var(--border-radius-base);
|
||||
user-select: none;
|
||||
|
||||
.item-title-root {
|
||||
position: absolute;
|
||||
left: 45px;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
.el-menu-item {
|
||||
height: var(--menu-item-height);
|
||||
line-height: var(--menu-item-height);
|
||||
padding-left: var(--spacing-l) !important;
|
||||
min-width: auto;
|
||||
|
||||
svg {
|
||||
width: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu .el-menu-item {
|
||||
height: var(--submenu-item-height);
|
||||
margin: 4px 0 !important;
|
||||
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-popper{
|
||||
.el-menu-item {
|
||||
--menu-item-height: 35px;
|
||||
--submenu-item-height: 27px;
|
||||
--menu-item-hover-fill: var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
margin: 0 var(--spacing-2xs);
|
||||
|
||||
.item-title {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
}
|
||||
|
||||
.svg-inline--fa {
|
||||
color: var(--color-text-light);
|
||||
position: relative;
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.svg-inline--fa {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.sideMenu {
|
||||
height: 100%;
|
||||
|
||||
&.sideMenuCollapsed {
|
||||
.fullName,
|
||||
:global(.item-title-root),
|
||||
:global(.el-menu--collapse) :global(.el-submenu__icon-arrow),
|
||||
:global(.el-icon-arrow-down) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
svg { color: var(--color-text-dark) !important; }
|
||||
}
|
||||
|
||||
.userSubmenu::before {
|
||||
width: 160%;
|
||||
}
|
||||
}
|
||||
|
||||
svg:global(.svg-inline--fa) {
|
||||
position: relative;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
svg:global(.svg-inline--fa.fa-home) { left: 4px; }
|
||||
.executionsSubmenu svg:global(.svg-inline--fa),
|
||||
.credentialsSubmenu svg:global(.svg-inline--fa),
|
||||
.updatesSubmenu svg:global(.svg-inline--fa),
|
||||
.settingsSubmenu svg:global(.svg-inline--fa)
|
||||
{ left: 5px !important; }
|
||||
|
||||
.helpMenu{
|
||||
svg:global(.svg-inline--fa) { left: 6px !important; }
|
||||
:global(.el-menu--inline) { margin-bottom: 8px; }
|
||||
}
|
||||
}
|
||||
|
||||
.sideMenuWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: $sidebar-width;
|
||||
border-right: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||
transition: width 150ms ease-in-out;
|
||||
width: $sidebar-expanded-width;
|
||||
.logo {
|
||||
height: $header-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
&.expanded {
|
||||
width: $sidebar-expanded-width;
|
||||
|
||||
.icon {
|
||||
img {
|
||||
position: relative;
|
||||
left: 16px;
|
||||
left: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ul { height: 100%; }
|
||||
&.sideMenuCollapsed {
|
||||
width: $sidebar-width;
|
||||
|
||||
.logo img {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sideMenuCollapseButton {
|
||||
|
@ -645,167 +537,60 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
|
||||
.sideMenuFlexContainer {
|
||||
.updates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: calc(100% - $header-height);
|
||||
padding: var(--spacing-5xs) 0;
|
||||
}
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
cursor: pointer;
|
||||
|
||||
.sideMenuUpper, .sideMenuLower {
|
||||
padding: 0 var(--spacing-xs);
|
||||
}
|
||||
svg { color: var(--color-text-base) !important; }
|
||||
span {
|
||||
display: none;
|
||||
&.expanded { display: initial; }
|
||||
}
|
||||
|
||||
|
||||
li:global(.is-active) {
|
||||
.sideMenuLower &, &.disableActiveStyle {
|
||||
background-color: initial;
|
||||
color: var(--color-text-base);
|
||||
|
||||
svg {
|
||||
color: var(--color-text-base) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-foreground-base);
|
||||
svg {
|
||||
color: var(--color-text-dark) !important;
|
||||
}
|
||||
&:global(.el-submenu) {
|
||||
background-color: unset;
|
||||
}
|
||||
&:hover {
|
||||
&, & svg {
|
||||
color: var(--color-text-dark) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logoItem {
|
||||
.userArea {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: $header-height;
|
||||
line-height: $header-height;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-background-xlight);
|
||||
cursor: default;
|
||||
padding: var(--spacing-xs);
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
border-top: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||
|
||||
&:hover, &:global(.is-active):hover {
|
||||
background-color: initial !important;
|
||||
}
|
||||
.userName {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
* { vertical-align: middle; }
|
||||
.icon {
|
||||
height: 18px;
|
||||
position: relative;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.footerMenuItems {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: var(-spacing-m);
|
||||
|
||||
&.loggedIn {
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.aboutIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.updatesSubmenu {
|
||||
margin-top: 0 !important;
|
||||
|
||||
.updatesLabel {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.giftContainer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
div > div { right: -.5em; }
|
||||
}
|
||||
}
|
||||
|
||||
.userSubmenu {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
padding: var(--spacing-xs) !important;
|
||||
margin: 0 !important;
|
||||
|
||||
// Fake border-top on user area
|
||||
&::before {
|
||||
width: 114%;
|
||||
border-top: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -12px;
|
||||
}
|
||||
|
||||
&:hover, &:global(.is-active) {
|
||||
background-color: initial !important;
|
||||
|
||||
.userActions svg {
|
||||
color: var(--color-text-base) !important;
|
||||
&.expanded {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
left: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 16px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.username {
|
||||
position: relative !important;
|
||||
display: flex !important;
|
||||
left: 9px !important;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
padding-top: var(--spacing-s);
|
||||
cursor: default;
|
||||
|
||||
.fullName {
|
||||
width: 99px;
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.userActions {
|
||||
position: relative;
|
||||
left: -1px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
svg { color: initial; }
|
||||
&.expanded {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userActionsMenu {
|
||||
margin-left: 25px !important;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 470px) {
|
||||
.helpMenu { display: none; }
|
||||
:global(#help) { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<n8n-menu-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:index="item.id"
|
||||
@click="onClick(item)"
|
||||
>
|
||||
<font-awesome-icon :icon="item.properties.icon" />
|
||||
<span slot="title" :class="{'item-title-root': root, 'item-title': !root}">{{ item.properties.title }}</span>
|
||||
</n8n-menu-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { IMenuItem } from '../Interface';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'MenuItemsIterator',
|
||||
props: [
|
||||
'items',
|
||||
'root',
|
||||
'afterItemClick',
|
||||
],
|
||||
methods: {
|
||||
onClick(item: IMenuItem) {
|
||||
if (item && item.type === 'link' && item.properties) {
|
||||
const href = item.properties.href;
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.properties.newWindow) {
|
||||
window.open(href);
|
||||
}
|
||||
else {
|
||||
window.location.assign(item.properties.href);
|
||||
}
|
||||
|
||||
if(this.afterItemClick) {
|
||||
this.afterItemClick(item.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,53 +1,22 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<n8n-menu :router="true" :default-active="$route.path" type="secondary">
|
||||
<div :class="$style.returnButton" @click="onReturn">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="arrow-left" />
|
||||
</i>
|
||||
<n8n-heading slot="title" size="large" :class="$style.settingsHeading" :bold="true">{{ $locale.baseText('settings') }}</n8n-heading>
|
||||
</div>
|
||||
<n8n-menu-item index="/settings/personal" v-if="canAccessPersonalSettings()" :class="$style.tab">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="user-circle" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText('settings.personal') }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="/settings/users" v-if="canAccessUsersSettings()" :class="[$style.tab, $style.usersMenu]">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="user-friends" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText('settings.users') }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="/settings/api" v-if="canAccessApiSettings()" :class="[$style.tab, $style.apiMenu]">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="plug" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText('settings.n8napi') }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item
|
||||
v-for="fakeDoor in settingsFakeDoorFeatures"
|
||||
v-bind:key="fakeDoor.featureName"
|
||||
:index="`/settings/coming-soon/${fakeDoor.id}`"
|
||||
:class="$style.tab"
|
||||
>
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon :icon="fakeDoor.icon" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText(fakeDoor.featureName) }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="/settings/community-nodes" v-if="canAccessCommunityNodes()" :class="$style.tab">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="cube" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText('settings.communityNodes') }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu :items="sidebarMenuItems" @select="handleSelect">
|
||||
<template #header>
|
||||
<div :class="$style.returnButton" @click="onReturn">
|
||||
<i class="mr-xs">
|
||||
<font-awesome-icon icon="arrow-left" />
|
||||
</i>
|
||||
<n8n-heading slot="title" size="large" :class="$style.settingsHeading" :bold="true">{{ $locale.baseText('settings') }}</n8n-heading>
|
||||
</div>
|
||||
</template>
|
||||
<template #menuSuffix>
|
||||
<div :class="$style.versionContainer">
|
||||
<n8n-link @click="onVersionClick" size="small">
|
||||
{{ $locale.baseText('settings.version') }} {{ versionCli }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-menu>
|
||||
<div :class="$style.versionContainer">
|
||||
<n8n-link @click="onVersionClick" size="small">
|
||||
{{ $locale.baseText('settings.version') }} {{ versionCli }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
:global(.el-menu) {
|
||||
--menu-item-height: 35px;
|
||||
--submenu-item-height: 27px;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
min-width: $sidebar-expanded-width;
|
||||
height: 100vh;
|
||||
background-color: var(--color-background-xlight);
|
||||
border-right: var(--border-base);
|
||||
position: relative;
|
||||
padding: var(--spacing-xs);
|
||||
overflow: auto;
|
||||
|
||||
ul {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.el-menu-item) > span{
|
||||
position: relative;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsHeading {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
svg:global(.svg-inline--fa) { position: relative; }
|
||||
}
|
||||
|
||||
.returnButton {
|
||||
composes: tab;
|
||||
margin-bottom: var(--spacing-l);
|
||||
padding: 0 var(--spacing-xs);
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
&:hover > * {
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.usersMenu svg { left: -2px; }
|
||||
.apiMenu svg { left: 2px; }
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
display: inline-flex;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.versionContainer {
|
||||
position: absolute;
|
||||
left: 23px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 420px) {
|
||||
.updatesSubmenu, .versionContainer { display: none; }
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
faFolderOpen,
|
||||
faGlobeAmericas,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faGraduationCap,
|
||||
faGripVertical,
|
||||
faHdd,
|
||||
|
@ -170,6 +171,7 @@ addIcon(faFilePdf);
|
|||
addIcon(faFilter);
|
||||
addIcon(faFolderOpen);
|
||||
addIcon(faGift);
|
||||
addIcon(faGlobe);
|
||||
addIcon(faGlobeAmericas);
|
||||
addIcon(faGraduationCap);
|
||||
addIcon(faHdd);
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
IWorkflowDb,
|
||||
XYPosition,
|
||||
IRestApiContext,
|
||||
IWorkflowsState,
|
||||
} from './Interface';
|
||||
|
||||
import nodeTypes from './modules/nodeTypes';
|
||||
|
|
|
@ -14,24 +14,9 @@
|
|||
</div>
|
||||
|
||||
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]">
|
||||
<n8n-menu default-active="owner" type="secondary" @select="onSelectOwner" ref="selectOwnerMenu">
|
||||
<n8n-menu-item index="owner">
|
||||
<template #title>
|
||||
<n8n-icon icon="user"/>
|
||||
<span class="ml-xs">
|
||||
{{ $locale.baseText('credentials.menu.myCredentials') }}
|
||||
</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="all">
|
||||
<template #title>
|
||||
<n8n-icon icon="globe-americas"/>
|
||||
<span class="ml-xs">
|
||||
{{ $locale.baseText('credentials.menu.allCredentials') }}
|
||||
</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</n8n-menu>
|
||||
<div :class="$style.sidebarContainer">
|
||||
<n8n-menu :items="menuItems" mode="tabs" @select="onSelectOwner" ref="selectOwnerMenu"></n8n-menu>
|
||||
</div>
|
||||
</enterprise-edition>
|
||||
</template>
|
||||
|
||||
|
@ -256,6 +241,7 @@ import {EnterpriseEditionFeature} from "@/constants";
|
|||
import TemplateCard from "@/components/TemplateCard.vue";
|
||||
import Vue from "vue";
|
||||
import { debounceHelper } from '@/components/mixins/debounce';
|
||||
import { IMenuItem } from 'n8n-design-system';
|
||||
|
||||
export default mixins(
|
||||
showMessage,
|
||||
|
@ -290,6 +276,22 @@ export default mixins(
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
menuItems(): IMenuItem[] {
|
||||
return [
|
||||
{
|
||||
id: 'owner',
|
||||
icon: 'user',
|
||||
label: this.$locale.baseText('credentials.menu.myCredentials'),
|
||||
position: 'top',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
icon: 'globe-americas',
|
||||
label: this.$locale.baseText('credentials.menu.allCredentials'),
|
||||
position: 'top',
|
||||
},
|
||||
];
|
||||
},
|
||||
currentUser(): IUser {
|
||||
return this.$store.getters['users/currentUser'];
|
||||
},
|
||||
|
@ -561,5 +563,9 @@ export default mixins(
|
|||
.type-input {
|
||||
--max-width: 265px;
|
||||
}
|
||||
|
||||
.sidebarContainer ul {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
Loading…
Reference in a new issue