mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(editor): Main navigation redesign (#4144)
* 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 * 👌 Addressing main sidebar product review comments * 💄 Updating css variable names after vite merge * ✔️ Fixing linting errors in the design system * ✔️ Fixing `element-ui` type import * 👌 Addressing the code review comments. * ✨ Adding link to new credentials view, removed old modal * 💄 Updating credentials view responsiveness and route highlight handling * 💄 Adding highlight to workflows submenu when on new workflow page * 💄 Updated active submenu text color
This commit is contained in:
committed by
GitHub
parent
e6e4f297c6
commit
3db53a1934
@@ -70,6 +70,10 @@
|
||||
:disabled="isWorkflowSaving"
|
||||
@click="onSaveButtonClick"
|
||||
/>
|
||||
<div :class="$style.workflowMenuContainer">
|
||||
<input :class="$style.hiddenInput" type="file" ref="importFile" @change="handleFileImport()">
|
||||
<n8n-action-dropdown :items="workflowMenuItems" @select="onWorkflowMenuSelect" />
|
||||
</div>
|
||||
</template>
|
||||
</PushConnectionTracker>
|
||||
</div>
|
||||
@@ -79,7 +83,12 @@
|
||||
import Vue from "vue";
|
||||
import mixins from "vue-typed-mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants";
|
||||
import {
|
||||
DUPLICATE_MODAL_KEY,
|
||||
MAX_WORKFLOW_NAME_LENGTH,
|
||||
VIEWS, WORKFLOW_MENU_ACTIONS,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
} from "@/constants";
|
||||
|
||||
import ShortenName from "@/components/ShortenName.vue";
|
||||
import TagsContainer from "@/components/TagsContainer.vue";
|
||||
@@ -90,6 +99,11 @@ import SaveButton from "@/components/SaveButton.vue";
|
||||
import TagsDropdown from "@/components/TagsDropdown.vue";
|
||||
import InlineTextEdit from "@/components/InlineTextEdit.vue";
|
||||
import BreakpointsObserver from "@/components/BreakpointsObserver.vue";
|
||||
import { IWorkflowDataUpdate, IWorkflowToShare } from "@/Interface";
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import { titleChange } from "../mixins/titleChange";
|
||||
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||
|
||||
const hasChanged = (prev: string[], curr: string[]) => {
|
||||
if (prev.length !== curr.length) {
|
||||
@@ -100,7 +114,7 @@ const hasChanged = (prev: string[], curr: string[]) => {
|
||||
return curr.reduce((accu, val) => accu || !set.has(val), false);
|
||||
};
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
export default mixins(workflowHelpers, titleChange).extend({
|
||||
name: "WorkflowDetails",
|
||||
components: {
|
||||
TagsContainer,
|
||||
@@ -139,6 +153,51 @@ export default mixins(workflowHelpers).extend({
|
||||
currentWorkflowId(): string {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
currentWorkflow (): string {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
workflowName (): string {
|
||||
return this.$store.getters.workflowName;
|
||||
},
|
||||
onWorkflowPage(): boolean {
|
||||
return this.$route.meta && this.$route.meta.nodeView;
|
||||
},
|
||||
workflowMenuItems(): Array<{}> {
|
||||
return [
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.DUPLICATE,
|
||||
label: this.$locale.baseText('menuActions.duplicate'),
|
||||
disabled: !this.onWorkflowPage || !this.currentWorkflow,
|
||||
},
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.DOWNLOAD,
|
||||
label: this.$locale.baseText('menuActions.download'),
|
||||
disabled: !this.onWorkflowPage,
|
||||
},
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL,
|
||||
label: this.$locale.baseText('menuActions.importFromUrl'),
|
||||
disabled: !this.onWorkflowPage,
|
||||
},
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.IMPORT_FROM_FILE,
|
||||
label: this.$locale.baseText('menuActions.importFromFile'),
|
||||
disabled: !this.onWorkflowPage,
|
||||
},
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.SETTINGS,
|
||||
label: this.$locale.baseText('generic.settings'),
|
||||
disabled: !this.onWorkflowPage || !this.currentWorkflow,
|
||||
},
|
||||
{
|
||||
id: WORKFLOW_MENU_ACTIONS.DELETE,
|
||||
label: this.$locale.baseText('menuActions.delete'),
|
||||
disabled: !this.onWorkflowPage || !this.currentWorkflow,
|
||||
customClass: this.$style.deleteItem,
|
||||
divided: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onSaveButtonClick () {
|
||||
@@ -220,6 +279,132 @@ export default mixins(workflowHelpers).extend({
|
||||
}
|
||||
cb(saved);
|
||||
},
|
||||
async handleFileImport(): Promise<void> {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event: ProgressEvent) => {
|
||||
const data = (event.target as FileReader).result;
|
||||
|
||||
let workflowData: IWorkflowDataUpdate;
|
||||
try {
|
||||
workflowData = JSON.parse(data as string);
|
||||
} catch (error) {
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.message'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$emit('importWorkflowData', { data: workflowData });
|
||||
};
|
||||
|
||||
const input = this.$refs.importFile as HTMLInputElement;
|
||||
if (input !== null && input.files !== null && input.files.length !== 0) {
|
||||
reader.readAsText(input!.files[0]!);
|
||||
}
|
||||
},
|
||||
async onWorkflowMenuSelect(action: string): Promise<void> {
|
||||
switch (action) {
|
||||
case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
|
||||
this.$store.dispatch('ui/openModal', DUPLICATE_MODAL_KEY);
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.DOWNLOAD: {
|
||||
const workflowData = await this.getWorkflowDataToSave();
|
||||
const {tags, ...data} = workflowData;
|
||||
if (data.id && typeof data.id === 'string') {
|
||||
data.id = parseInt(data.id, 10);
|
||||
}
|
||||
|
||||
const exportData: IWorkflowToShare = {
|
||||
...data,
|
||||
meta: {
|
||||
instanceId: this.$store.getters.instanceId,
|
||||
},
|
||||
tags: (tags || []).map(tagId => {
|
||||
const {usageCount, ...tag} = this.$store.getters["tags/getTagById"](tagId);
|
||||
|
||||
return tag;
|
||||
}),
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
|
||||
let workflowName = this.$store.getters.workflowName || 'unsaved_workflow';
|
||||
workflowName = workflowName.replace(/[^a-z0-9]/gi, '_');
|
||||
|
||||
this.$telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
||||
saveAs(blob, workflowName + '.json');
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL: {
|
||||
try {
|
||||
const promptResponse = await this.$prompt(
|
||||
this.$locale.baseText('mainSidebar.prompt.workflowUrl') + ':',
|
||||
this.$locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':',
|
||||
{
|
||||
confirmButtonText: this.$locale.baseText('mainSidebar.prompt.import'),
|
||||
cancelButtonText: this.$locale.baseText('mainSidebar.prompt.cancel'),
|
||||
inputErrorMessage: this.$locale.baseText('mainSidebar.prompt.invalidUrl'),
|
||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
||||
},
|
||||
) as MessageBoxInputData;
|
||||
|
||||
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
|
||||
} catch (e) {}
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_FILE: {
|
||||
(this.$refs.importFile as HTMLInputElement).click();
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.SETTINGS: {
|
||||
this.$store.dispatch('ui/openModal', WORKFLOW_SETTINGS_MODAL_KEY);
|
||||
break;
|
||||
}
|
||||
case WORKFLOW_MENU_ACTIONS.DELETE: {
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.message',
|
||||
{ interpolate: { workflowName: this.workflowName } },
|
||||
),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.restApi().deleteWorkflow(this.currentWorkflow);
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$store.commit('setStateDirty', false);
|
||||
// Reset tab title since workflow is deleted.
|
||||
this.$titleReset();
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
this.$router.push({ name: VIEWS.NEW_WORKFLOW });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentWorkflowId() {
|
||||
@@ -235,6 +420,8 @@ $--text-line-height: 24px;
|
||||
$--header-spacing: 20px;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -291,3 +478,17 @@ $--header-spacing: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
.workflowMenuContainer {
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.hiddenInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.deleteItem {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user