mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
perf(editor): Fix WorkflowDetails excessive re-rendering (#10767)
This commit is contained in:
parent
21936c88a8
commit
00013a2069
|
@ -164,7 +164,16 @@ async function navigateToExecutionsView(openInNewTab: boolean) {
|
||||||
<div>
|
<div>
|
||||||
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
|
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
|
||||||
<div v-show="!hideMenuBar" class="top-menu">
|
<div v-show="!hideMenuBar" class="top-menu">
|
||||||
<WorkflowDetails v-if="workflow?.name" :workflow="workflow" :read-only="readOnly" />
|
<WorkflowDetails
|
||||||
|
v-if="workflow?.name"
|
||||||
|
:id="workflow.id"
|
||||||
|
:tags="workflow.tags"
|
||||||
|
:name="workflow.name"
|
||||||
|
:meta="workflow.meta"
|
||||||
|
:scopes="workflow.scopes"
|
||||||
|
:active="workflow.active"
|
||||||
|
:read-only="readOnly"
|
||||||
|
/>
|
||||||
<TabBar
|
<TabBar
|
||||||
v-if="onWorkflowPage"
|
v-if="onWorkflowPage"
|
||||||
:items="tabBarItems"
|
:items="tabBarItems"
|
||||||
|
|
|
@ -59,7 +59,7 @@ describe('WorkflowDetails', () => {
|
||||||
it('renders workflow name and tags', async () => {
|
it('renders workflow name and tags', async () => {
|
||||||
const { getByTestId, getByText } = renderComponent({
|
const { getByTestId, getByText } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
workflow,
|
...workflow,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -76,7 +76,7 @@ describe('WorkflowDetails', () => {
|
||||||
const onSaveButtonClick = vi.fn();
|
const onSaveButtonClick = vi.fn();
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
workflow,
|
...workflow,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
|
@ -95,7 +95,7 @@ describe('WorkflowDetails', () => {
|
||||||
|
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
workflow,
|
...workflow,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,8 +58,13 @@ import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
workflow: IWorkflowDb;
|
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
id: IWorkflowDb['id'];
|
||||||
|
tags: IWorkflowDb['tags'];
|
||||||
|
name: IWorkflowDb['name'];
|
||||||
|
meta: IWorkflowDb['meta'];
|
||||||
|
scopes: IWorkflowDb['scopes'];
|
||||||
|
active: IWorkflowDb['active'];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
@ -115,11 +120,7 @@ const hasChanged = (prev: string[], curr: string[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNewWorkflow = computed(() => {
|
const isNewWorkflow = computed(() => {
|
||||||
return (
|
return !props.id || props.id === PLACEHOLDER_EMPTY_WORKFLOW_ID || props.id === 'new';
|
||||||
!props.workflow.id ||
|
|
||||||
props.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
|
|
||||||
props.workflow.id === 'new'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isWorkflowSaving = computed(() => {
|
const isWorkflowSaving = computed(() => {
|
||||||
|
@ -138,9 +139,7 @@ const onExecutionsTab = computed(() => {
|
||||||
].includes((route.name as string) || '');
|
].includes((route.name as string) || '');
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflowPermissions = computed(
|
const workflowPermissions = computed(() => getResourcePermissions(props.scopes).workflow);
|
||||||
() => getResourcePermissions(workflowsStore.getWorkflowById(props.workflow.id)?.scopes).workflow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
||||||
const actions: ActionDropdownItem[] = [
|
const actions: ActionDropdownItem[] = [
|
||||||
|
@ -155,7 +154,7 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
|
||||||
actions.unshift({
|
actions.unshift({
|
||||||
id: WORKFLOW_MENU_ACTIONS.DUPLICATE,
|
id: WORKFLOW_MENU_ACTIONS.DUPLICATE,
|
||||||
label: locale.baseText('menuActions.duplicate'),
|
label: locale.baseText('menuActions.duplicate'),
|
||||||
disabled: !onWorkflowPage.value || !props.workflow.id,
|
disabled: !onWorkflowPage.value || !props.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push(
|
actions.push(
|
||||||
|
@ -219,11 +218,11 @@ const isWorkflowHistoryFeatureEnabled = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflowTagIds = computed(() => {
|
const workflowTagIds = computed(() => {
|
||||||
return (props.workflow.tags ?? []).map((tag) => (typeof tag === 'string' ? tag : tag.id));
|
return (props.tags ?? []).map((tag) => (typeof tag === 'string' ? tag : tag.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.workflow.id,
|
() => props.id,
|
||||||
() => {
|
() => {
|
||||||
isTagsEditEnabled.value = false;
|
isTagsEditEnabled.value = false;
|
||||||
isNameEditEnabled.value = false;
|
isNameEditEnabled.value = false;
|
||||||
|
@ -232,8 +231,8 @@ watch(
|
||||||
|
|
||||||
function getWorkflowId(): string | undefined {
|
function getWorkflowId(): string | undefined {
|
||||||
let id: string | undefined = undefined;
|
let id: string | undefined = undefined;
|
||||||
if (props.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
if (props.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||||
id = props.workflow.id;
|
id = props.id;
|
||||||
} else if (route.params.name && route.params.name !== 'new') {
|
} else if (route.params.name && route.params.name !== 'new') {
|
||||||
id = route.params.name as string;
|
id = route.params.name as string;
|
||||||
}
|
}
|
||||||
|
@ -249,8 +248,8 @@ async function onSaveButtonClick() {
|
||||||
|
|
||||||
const id = getWorkflowId();
|
const id = getWorkflowId();
|
||||||
|
|
||||||
const name = props.workflow.name;
|
const name = props.name;
|
||||||
const tags = props.workflow.tags as string[];
|
const tags = props.tags as string[];
|
||||||
|
|
||||||
const saved = await workflowHelpers.saveCurrentWorkflow({
|
const saved = await workflowHelpers.saveCurrentWorkflow({
|
||||||
id,
|
id,
|
||||||
|
@ -266,7 +265,7 @@ async function onSaveButtonClick() {
|
||||||
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
||||||
await router.replace({
|
await router.replace({
|
||||||
name: VIEWS.WORKFLOW,
|
name: VIEWS.WORKFLOW,
|
||||||
params: { name: props.workflow.id },
|
params: { name: props.id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,18 +274,18 @@ async function onSaveButtonClick() {
|
||||||
function onShareButtonClick() {
|
function onShareButtonClick() {
|
||||||
uiStore.openModalWithData({
|
uiStore.openModalWithData({
|
||||||
name: WORKFLOW_SHARE_MODAL_KEY,
|
name: WORKFLOW_SHARE_MODAL_KEY,
|
||||||
data: { id: props.workflow.id },
|
data: { id: props.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
telemetry.track('User opened sharing modal', {
|
telemetry.track('User opened sharing modal', {
|
||||||
workflow_id: props.workflow.id,
|
workflow_id: props.id,
|
||||||
user_id_sharer: usersStore.currentUser?.id,
|
user_id_sharer: usersStore.currentUser?.id,
|
||||||
sub_view: route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor',
|
sub_view: route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTagsEditEnable() {
|
function onTagsEditEnable() {
|
||||||
appliedTagIds.value = (props.workflow.tags ?? []) as string[];
|
appliedTagIds.value = (props.tags ?? []) as string[];
|
||||||
isTagsEditEnabled.value = true;
|
isTagsEditEnabled.value = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -297,7 +296,7 @@ function onTagsEditEnable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTagsBlur() {
|
async function onTagsBlur() {
|
||||||
const current = (props.workflow.tags ?? []) as string[];
|
const current = (props.tags ?? []) as string[];
|
||||||
const tags = appliedTagIds.value;
|
const tags = appliedTagIds.value;
|
||||||
if (!hasChanged(current, tags)) {
|
if (!hasChanged(current, tags)) {
|
||||||
isTagsEditEnabled.value = false;
|
isTagsEditEnabled.value = false;
|
||||||
|
@ -311,7 +310,7 @@ async function onTagsBlur() {
|
||||||
|
|
||||||
const saved = await workflowHelpers.saveCurrentWorkflow({ tags });
|
const saved = await workflowHelpers.saveCurrentWorkflow({ tags });
|
||||||
telemetry.track('User edited workflow tags', {
|
telemetry.track('User edited workflow tags', {
|
||||||
workflow_id: props.workflow.id,
|
workflow_id: props.id,
|
||||||
new_tag_count: tags.length,
|
new_tag_count: tags.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -355,7 +354,7 @@ async function onNameSubmit({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newName === props.workflow.name) {
|
if (newName === props.name) {
|
||||||
isNameEditEnabled.value = false;
|
isNameEditEnabled.value = false;
|
||||||
|
|
||||||
onSubmit(true);
|
onSubmit(true);
|
||||||
|
@ -405,9 +404,9 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
uiStore.openModalWithData({
|
uiStore.openModalWithData({
|
||||||
name: DUPLICATE_MODAL_KEY,
|
name: DUPLICATE_MODAL_KEY,
|
||||||
data: {
|
data: {
|
||||||
id: props.workflow.id,
|
id: props.id,
|
||||||
name: props.workflow.name,
|
name: props.name,
|
||||||
tags: props.workflow.tags,
|
tags: props.tags,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -418,7 +417,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
const exportData: IWorkflowToShare = {
|
const exportData: IWorkflowToShare = {
|
||||||
...data,
|
...data,
|
||||||
meta: {
|
meta: {
|
||||||
...props.workflow.meta,
|
...props.meta,
|
||||||
instanceId: rootStore.instanceId,
|
instanceId: rootStore.instanceId,
|
||||||
},
|
},
|
||||||
tags: (tags ?? []).map((tagId) => {
|
tags: (tags ?? []).map((tagId) => {
|
||||||
|
@ -432,7 +431,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
type: 'application/json;charset=utf-8',
|
type: 'application/json;charset=utf-8',
|
||||||
});
|
});
|
||||||
|
|
||||||
let name = props.workflow.name || 'unsaved_workflow';
|
let name = props.name || 'unsaved_workflow';
|
||||||
name = name.replace(/[^a-z0-9]/gi, '_');
|
name = name.replace(/[^a-z0-9]/gi, '_');
|
||||||
|
|
||||||
telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
||||||
|
@ -533,7 +532,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
case WORKFLOW_MENU_ACTIONS.DELETE: {
|
case WORKFLOW_MENU_ACTIONS.DELETE: {
|
||||||
const deleteConfirmed = await message.confirm(
|
const deleteConfirmed = await message.confirm(
|
||||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {
|
||||||
interpolate: { workflowName: props.workflow.name },
|
interpolate: { workflowName: props.name },
|
||||||
}),
|
}),
|
||||||
locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||||
{
|
{
|
||||||
|
@ -552,7 +551,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await workflowsStore.deleteWorkflow(props.workflow.id);
|
await workflowsStore.deleteWorkflow(props.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, locale.baseText('generic.deleteWorkflowError'));
|
toast.showError(error, locale.baseText('generic.deleteWorkflowError'));
|
||||||
return;
|
return;
|
||||||
|
@ -608,15 +607,10 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<BreakpointsObserver :value-x-s="15" :value-s-m="25" :value-m-d="50" class="name-container">
|
<BreakpointsObserver :value-x-s="15" :value-s-m="25" :value-m-d="50" class="name-container">
|
||||||
<template #default="{ value }">
|
<template #default="{ value }">
|
||||||
<ShortenName
|
<ShortenName :name="name" :limit="value" :custom="true" test-id="workflow-name-input">
|
||||||
:name="workflow.name"
|
|
||||||
:limit="value"
|
|
||||||
:custom="true"
|
|
||||||
test-id="workflow-name-input"
|
|
||||||
>
|
|
||||||
<template #default="{ shortenedName }">
|
<template #default="{ shortenedName }">
|
||||||
<InlineTextEdit
|
<InlineTextEdit
|
||||||
:model-value="workflow.name"
|
:model-value="name"
|
||||||
:preview-value="shortenedName"
|
:preview-value="shortenedName"
|
||||||
:is-edit-enabled="isNameEditEnabled"
|
:is-edit-enabled="isNameEditEnabled"
|
||||||
:max-length="MAX_WORKFLOW_NAME_LENGTH"
|
:max-length="MAX_WORKFLOW_NAME_LENGTH"
|
||||||
|
@ -645,9 +639,7 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(workflow.tags ?? []).length === 0 &&
|
(tags ?? []).length === 0 && !readOnly && (isNewWorkflow || workflowPermissions.update)
|
||||||
!readOnly &&
|
|
||||||
(isNewWorkflow || workflowPermissions.update)
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<span class="add-tag clickable" data-test-id="new-tag-link" @click="onTagsEditEnable">
|
<span class="add-tag clickable" data-test-id="new-tag-link" @click="onTagsEditEnable">
|
||||||
|
@ -656,7 +648,7 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
</div>
|
</div>
|
||||||
<WorkflowTagsContainer
|
<WorkflowTagsContainer
|
||||||
v-else
|
v-else
|
||||||
:key="workflow.id"
|
:key="id"
|
||||||
:tag-ids="workflowTagIds"
|
:tag-ids="workflowTagIds"
|
||||||
:clickable="true"
|
:clickable="true"
|
||||||
:responsive="true"
|
:responsive="true"
|
||||||
|
@ -669,8 +661,8 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
<PushConnectionTracker class="actions">
|
<PushConnectionTracker class="actions">
|
||||||
<span :class="`activator ${$style.group}`">
|
<span :class="`activator ${$style.group}`">
|
||||||
<WorkflowActivator
|
<WorkflowActivator
|
||||||
:workflow-active="workflow.active"
|
:workflow-active="active"
|
||||||
:workflow-id="workflow.id"
|
:workflow-id="id"
|
||||||
:workflow-permissions="workflowPermissions"
|
:workflow-permissions="workflowPermissions"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -727,7 +719,7 @@ function showCreateWorkflowSuccessToast(id?: string) {
|
||||||
@click="onSaveButtonClick"
|
@click="onSaveButtonClick"
|
||||||
/>
|
/>
|
||||||
<WorkflowHistoryButton
|
<WorkflowHistoryButton
|
||||||
:workflow-id="props.workflow.id"
|
:workflow-id="props.id"
|
||||||
:is-feature-enabled="isWorkflowHistoryFeatureEnabled"
|
:is-feature-enabled="isWorkflowHistoryFeatureEnabled"
|
||||||
:is-new-workflow="isNewWorkflow"
|
:is-new-workflow="isNewWorkflow"
|
||||||
@upgrade="goToWorkflowHistoryUpgrade"
|
@upgrade="goToWorkflowHistoryUpgrade"
|
||||||
|
|
Loading…
Reference in a new issue