mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix sidebar logo container layout (#13203)
This commit is contained in:
parent
1909b74350
commit
850d458858
|
@ -65,9 +65,7 @@ onMounted(() => {
|
||||||
<div :class="containerClasses" data-test-id="n8n-logo">
|
<div :class="containerClasses" data-test-id="n8n-logo">
|
||||||
<LogoIcon :class="$style.logo" ref="logo" />
|
<LogoIcon :class="$style.logo" ref="logo" />
|
||||||
<LogoText v-if="showLogoText" :class="$style.logoText" />
|
<LogoText v-if="showLogoText" :class="$style.logoText" />
|
||||||
<div v-if="showReleaseChannelTag" size="small" round :class="$style.releaseChannelTag">
|
<div v-if="showReleaseChannelTag" :class="$style.releaseChannelTag">{{ releaseChannel }}</div>
|
||||||
{{ releaseChannel }}
|
|
||||||
</div>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,6 +74,7 @@ onMounted(() => {
|
||||||
.logoContainer {
|
.logoContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logoText {
|
.logoText {
|
||||||
|
@ -91,12 +90,11 @@ onMounted(() => {
|
||||||
background-color: var(--color-background-base);
|
background-color: var(--color-background-base);
|
||||||
border: 1px solid var(--color-foreground-base);
|
border: 1px solid var(--color-foreground-base);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
font-size: var(--font-size-4xs);
|
font-size: var(--font-size-3xs);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
line-height: var(--font-line-height-regular);
|
line-height: var(--font-line-height-regular);
|
||||||
height: var(--spacing-s);
|
margin: 8px 0 0 3px;
|
||||||
margin: 10px 0 0 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.authView {
|
.authView {
|
||||||
|
@ -104,12 +102,18 @@ onMounted(() => {
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.logo,
|
||||||
|
.logoText {
|
||||||
transform: scale(1.3);
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarExpanded .logo {
|
.logoText {
|
||||||
margin-left: var(--spacing-xs);
|
margin-left: var(--spacing-xs);
|
||||||
|
margin-right: var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarExpanded .logo {
|
||||||
|
margin-left: var(--spacing-3xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarCollapsed .logo {
|
.sidebarCollapsed .logo {
|
||||||
|
|
|
@ -44,6 +44,6 @@ exports[`Logo > renders the releaseChannelTag for non-stable releaseChannel 1`]
|
||||||
<path d="M18.367 21.2h1.624v-3.442c0-1.131.685-1.627 1.46-1.627.76 0 1.357.509 1.357 1.55v3.52h1.624V17.35c0-1.664-.964-2.63-2.474-2.63-.952 0-1.485.381-1.865.877h-.102l-.14-.75h-1.484zm-14.376 0H2.367v-6.352h1.485l.14.75h.1c.381-.496.914-.877 1.866-.877 1.51 0 2.474.966 2.474 2.63v3.85H6.808V17.68c0-1.041-.596-1.55-1.358-1.55-.774 0-1.459.496-1.459 1.627z"></path>
|
<path d="M18.367 21.2h1.624v-3.442c0-1.131.685-1.627 1.46-1.627.76 0 1.357.509 1.357 1.55v3.52h1.624V17.35c0-1.664-.964-2.63-2.474-2.63-.952 0-1.485.381-1.865.877h-.102l-.14-.75h-1.484zm-14.376 0H2.367v-6.352h1.485l.14.75h.1c.381-.496.914-.877 1.866-.877 1.51 0 2.474.966 2.474 2.63v3.85H6.808V17.68c0-1.041-.596-1.55-1.358-1.55-.774 0-1.459.496-1.459 1.627z"></path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<div size="small" round="" class="releaseChannelTag">dev</div>
|
<div class="releaseChannelTag">dev</div>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { type MockedStore, mockedStore } from '@/__tests__/utils';
|
||||||
|
import { defaultSettings } from '@/__tests__/defaults';
|
||||||
|
import MainSidebar from '@/components/MainSidebar.vue';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({}),
|
||||||
|
useRoute: () => reactive({}),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let renderComponent: ReturnType<typeof createComponentRenderer>;
|
||||||
|
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||||
|
let uiStore: MockedStore<typeof useUIStore>;
|
||||||
|
let sourceControlStore: MockedStore<typeof useSourceControlStore>;
|
||||||
|
|
||||||
|
describe('MainSidebar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
renderComponent = createComponentRenderer(MainSidebar, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
});
|
||||||
|
settingsStore = mockedStore(useSettingsStore);
|
||||||
|
uiStore = mockedStore(useUIStore);
|
||||||
|
sourceControlStore = mockedStore(useSourceControlStore);
|
||||||
|
|
||||||
|
settingsStore.settings = defaultSettings;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the sidebar without error', () => {
|
||||||
|
expect(() => renderComponent()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[false, true, true],
|
||||||
|
[true, false, false],
|
||||||
|
[true, true, false],
|
||||||
|
[false, false, false],
|
||||||
|
])(
|
||||||
|
'should render readonly tooltip when is opened %s and the environment is readonly %s',
|
||||||
|
(sidebarMenuCollapsed, branchReadOnly, shouldRender) => {
|
||||||
|
uiStore.sidebarMenuCollapsed = sidebarMenuCollapsed;
|
||||||
|
sourceControlStore.preferences.branchReadOnly = branchReadOnly;
|
||||||
|
|
||||||
|
const { queryByTestId } = renderComponent();
|
||||||
|
|
||||||
|
expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
|
@ -339,7 +339,12 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</template>
|
</template>
|
||||||
<N8nIcon icon="lock" size="xsmall" :class="$style.readOnlyEnvironmentIcon" />
|
<N8nIcon
|
||||||
|
data-test-id="read-only-env-icon"
|
||||||
|
icon="lock"
|
||||||
|
size="xsmall"
|
||||||
|
:class="$style.readOnlyEnvironmentIcon"
|
||||||
|
/>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</Logo>
|
</Logo>
|
||||||
<N8nNavigationDropdown
|
<N8nNavigationDropdown
|
||||||
|
@ -368,7 +373,18 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #[createProjectAppendSlotName]="{ item }">
|
<template #[createProjectAppendSlotName]="{ item }">
|
||||||
<N8nTooltip v-if="item.disabled" placement="right" :content="projectsLimitReachedMessage">
|
<N8nTooltip
|
||||||
|
v-if="sourceControlStore.preferences.branchReadOnly"
|
||||||
|
placement="right"
|
||||||
|
:content="i18n.baseText('readOnlyEnv.cantAdd.project')"
|
||||||
|
>
|
||||||
|
<N8nIcon style="margin-left: auto; margin-right: 5px" icon="lock" size="xsmall" />
|
||||||
|
</N8nTooltip>
|
||||||
|
<N8nTooltip
|
||||||
|
v-else-if="item.disabled"
|
||||||
|
placement="right"
|
||||||
|
:content="projectsLimitReachedMessage"
|
||||||
|
>
|
||||||
<N8nButton
|
<N8nButton
|
||||||
:size="'mini'"
|
:size="'mini'"
|
||||||
style="margin-left: auto"
|
style="margin-left: auto"
|
||||||
|
@ -462,19 +478,17 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.sideMenu {
|
.sideMenu {
|
||||||
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
border-right: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
border-right: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
transition: width 150ms ease-in-out;
|
transition: width 150ms ease-in-out;
|
||||||
width: $sidebar-expanded-width;
|
min-width: $sidebar-expanded-width;
|
||||||
padding-top: 54px;
|
max-width: 244px;
|
||||||
background-color: var(--menu-background, var(--color-background-xlight));
|
background-color: var(--menu-background, var(--color-background-xlight));
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xs);
|
||||||
|
@ -489,7 +503,7 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||||
|
|
||||||
&.sideMenuCollapsed {
|
&.sideMenuCollapsed {
|
||||||
width: $sidebar-width;
|
width: $sidebar-width;
|
||||||
padding-top: 100px;
|
min-width: auto;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -589,6 +603,6 @@ onClickOutside(createBtn as Ref<VueInstance>, () => {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
margin: 5px 5px 0;
|
margin: 7px 12px 0 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -836,7 +836,7 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-display-wrapper {
|
.data-display-wrapper {
|
||||||
height: calc(100% - var(--spacing-l)) !important;
|
height: 100%;
|
||||||
margin-top: var(--spacing-xl) !important;
|
margin-top: var(--spacing-xl) !important;
|
||||||
margin-bottom: var(--spacing-xl) !important;
|
margin-bottom: var(--spacing-xl) !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -911,6 +911,7 @@
|
||||||
"readOnlyEnv.tooltip.link": "More info.",
|
"readOnlyEnv.tooltip.link": "More info.",
|
||||||
"readOnlyEnv.cantAdd.workflow": "You can't add new workflows to a protected n8n instance",
|
"readOnlyEnv.cantAdd.workflow": "You can't add new workflows to a protected n8n instance",
|
||||||
"readOnlyEnv.cantAdd.credential": "You can't add new credentials to a protected n8n instance",
|
"readOnlyEnv.cantAdd.credential": "You can't add new credentials to a protected n8n instance",
|
||||||
|
"readOnlyEnv.cantAdd.project": "You can't add new projects to a protected n8n instance",
|
||||||
"readOnlyEnv.cantAdd.any": "You can't create new workflows or credentials on a protected n8n instance",
|
"readOnlyEnv.cantAdd.any": "You can't create new workflows or credentials on a protected n8n instance",
|
||||||
"readOnlyEnv.cantEditOrRun": "This workflow can't be edited or run manually because it's on a protected instance",
|
"readOnlyEnv.cantEditOrRun": "This workflow can't be edited or run manually because it's on a protected instance",
|
||||||
"mainSidebar.aboutN8n": "About n8n",
|
"mainSidebar.aboutN8n": "About n8n",
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { STORES } from '@/constants';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
import type { CreateProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
import type { CreateProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
|
||||||
export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -22,6 +23,7 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
const sourceControlStore = useSourceControlStore();
|
||||||
|
|
||||||
const projects = ref<ProjectListItem[]>([]);
|
const projects = ref<ProjectListItem[]>([]);
|
||||||
const myProjects = ref<ProjectListItem[]>([]);
|
const myProjects = ref<ProjectListItem[]>([]);
|
||||||
|
@ -60,8 +62,9 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||||
);
|
);
|
||||||
const canCreateProjects = computed<boolean>(
|
const canCreateProjects = computed<boolean>(
|
||||||
() =>
|
() =>
|
||||||
hasUnlimitedProjects.value ||
|
(hasUnlimitedProjects.value ||
|
||||||
(isTeamProjectFeatureEnabled.value && !isTeamProjectLimitExceeded.value),
|
(isTeamProjectFeatureEnabled.value && !isTeamProjectLimitExceeded.value)) &&
|
||||||
|
!sourceControlStore.preferences.branchReadOnly,
|
||||||
);
|
);
|
||||||
const hasPermissionToCreateProjects = computed(() =>
|
const hasPermissionToCreateProjects = computed(() =>
|
||||||
hasPermission(['rbac'], { rbac: { scope: 'project:create' } }),
|
hasPermission(['rbac'], { rbac: { scope: 'project:create' } }),
|
||||||
|
|
Loading…
Reference in a new issue