feat(editor): Store focused panel state in local storage (no-changelog) (#17163)

This commit is contained in:
Daria
2025-07-10 15:04:31 +03:00
committed by GitHub
parent 4b945a028c
commit 5c723e1bdd
7 changed files with 100 additions and 38 deletions

View File

@@ -29,7 +29,6 @@ import { mock } from 'vitest-mock-extended';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useExecutionsStore } from '@/stores/executions.store';
import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useProjectsStore } from '@/stores/projects.store';
import { waitFor } from '@testing-library/vue';
@@ -2823,7 +2822,6 @@ describe('useCanvasOperations', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const executionsStore = mockedStore(useExecutionsStore);
const focusPanelStore = mockedStore(useFocusPanelStore);
const nodeHelpers = { credentialsUpdated: { value: true } };
@@ -2834,7 +2832,6 @@ describe('useCanvasOperations', () => {
workflowsStore.resetState = vi.fn();
workflowsStore.setActiveExecutionId = vi.fn();
uiStore.resetLastInteractedWith = vi.fn();
focusPanelStore.reset = vi.fn();
executionsStore.activeExecution = null;
workflowsStore.executionWaitingForWebhook = true;
@@ -2872,7 +2869,6 @@ describe('useCanvasOperations', () => {
expect(workflowsStore.resetState).toHaveBeenCalled();
expect(workflowsStore.currentWorkflowExecutions).toEqual([]);
expect(workflowsStore.setActiveExecutionId).toHaveBeenCalledWith(undefined);
expect(focusPanelStore.reset).toHaveBeenCalled();
expect(uiStore.resetLastInteractedWith).toHaveBeenCalled();
expect(uiStore.stateIsDirty).toBe(false);
expect(executionsStore.activeExecution).toBeNull();

View File

@@ -53,7 +53,6 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useTagsStore } from '@/stores/tags.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useFocusPanelStore } from '@/stores/focusPanel.store';
import type {
CanvasConnection,
CanvasConnectionCreateData,
@@ -153,7 +152,6 @@ export function useCanvasOperations() {
const settingsStore = useSettingsStore();
const tagsStore = useTagsStore();
const nodeCreatorStore = useNodeCreatorStore();
const focusPanelStore = useFocusPanelStore();
const executionsStore = useExecutionsStore();
const projectsStore = useProjectsStore();
const logsStore = useLogsStore();
@@ -1610,8 +1608,6 @@ export function useCanvasOperations() {
workflowsStore.currentWorkflowExecutions = [];
workflowsStore.setActiveExecutionId(undefined);
focusPanelStore.reset();
// Reset actions
uiStore.resetLastInteractedWith();
uiStore.stateIsDirty = false;

View File

@@ -71,11 +71,11 @@ describe('useNodeSettingsParameters', () => {
ndvStore.activeNodeName = 'Node1';
ndvStore.setActiveNodeName = vi.fn();
ndvStore.resetNDVPushRef = vi.fn();
focusPanelStore.setFocusedNodeParameter = vi.fn();
focusPanelStore.openWithFocusedNodeParameter = vi.fn();
focusPanelStore.focusPanelActive = false;
});
it('sets focused node parameter and activates panel', () => {
it('sets focused node parameter', () => {
const { handleFocus } = useNodeSettingsParameters();
const node: INodeUi = {
id: '1',
@@ -95,12 +95,11 @@ describe('useNodeSettingsParameters', () => {
handleFocus(node, path, parameter);
expect(focusPanelStore.setFocusedNodeParameter).toHaveBeenCalledWith({
expect(focusPanelStore.openWithFocusedNodeParameter).toHaveBeenCalledWith({
nodeId: node.id,
parameterPath: path,
parameter,
});
expect(focusPanelStore.focusPanelActive).toBe(true);
expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(null);
expect(ndvStore.resetNDVPushRef).toHaveBeenCalled();
@@ -118,7 +117,7 @@ describe('useNodeSettingsParameters', () => {
handleFocus(undefined, 'parameters.foo', parameter);
expect(focusPanelStore.setFocusedNodeParameter).not.toHaveBeenCalled();
expect(focusPanelStore.openWithFocusedNodeParameter).not.toHaveBeenCalled();
});
});
});

View File

@@ -233,7 +233,7 @@ export function useNodeSettingsParameters() {
const ndvStore = useNDVStore();
const focusPanelStore = useFocusPanelStore();
focusPanelStore.setFocusedNodeParameter({
focusPanelStore.openWithFocusedNodeParameter({
nodeId: node.id,
parameterPath: path,
parameter,
@@ -243,8 +243,6 @@ export function useNodeSettingsParameters() {
ndvStore.setActiveNodeName(null);
ndvStore.resetNDVPushRef();
}
focusPanelStore.focusPanelActive = true;
}
function shouldDisplayNodeParameter(

View File

@@ -26,6 +26,7 @@ import { useTelemetry } from './useTelemetry';
import { useNodeHelpers } from './useNodeHelpers';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useTemplatesStore } from '@/stores/templates.store';
import { useFocusPanelStore } from '@/stores/focusPanel.store';
export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRouter> }) {
const uiStore = useUIStore();
@@ -33,6 +34,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
const message = useMessage();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const focusPanelStore = useFocusPanelStore();
const nodeTypesStore = useNodeTypesStore();
const toast = useToast();
const telemetry = useTelemetry();
@@ -346,6 +348,8 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
workflowsStore.addWorkflow(workflowData);
focusPanelStore.onNewWorkflowSave(workflowData.id);
if (openInNewWindow) {
const routeData = router.resolve({
name: VIEWS.WORKFLOW,

View File

@@ -494,6 +494,7 @@ export const LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS =
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
export const LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT = 'N8N_DISMISSED_WHATS_NEW_CALLOUT';
export const LOCAL_STORAGE_NDV_PANEL_WIDTH = 'N8N_NDV_PANEL_WIDTH';
export const LOCAL_STORAGE_FOCUS_PANEL = 'N8N_FOCUS_PANEL';
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
export const COMMUNITY_PLUS_DOCS_URL =

View File

@@ -1,10 +1,17 @@
import { STORES } from '@n8n/stores';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { computed } from 'vue';
import get from 'lodash/get';
import { type NodeParameterValueType, type INode, type INodeProperties } from 'n8n-workflow';
import {
type NodeParameterValueType,
type INode,
type INodeProperties,
jsonParse,
} from 'n8n-workflow';
import { useWorkflowsStore } from './workflows.store';
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { useStorage } from '@/composables/useStorage';
type FocusedNodeParameter = {
nodeId: string;
@@ -17,11 +24,36 @@ export type RichFocusedNodeParameter = FocusedNodeParameter & {
value: NodeParameterValueType;
};
type FocusPanelData = {
isActive: boolean;
parameters: FocusedNodeParameter[];
};
type FocusPanelDataByWid = Record<string, FocusPanelData>;
const DEFAULT_FOCUS_PANEL_DATA: FocusPanelData = { isActive: false, parameters: [] };
export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
const workflowsStore = useWorkflowsStore();
const focusPanelStorage = useStorage(LOCAL_STORAGE_FOCUS_PANEL);
const focusPanelActive = ref(false);
const _focusedNodeParameters = ref<FocusedNodeParameter[]>([]);
const focusPanelData = computed((): FocusPanelDataByWid => {
const defaultValue: FocusPanelDataByWid = {
[workflowsStore.workflowId]: DEFAULT_FOCUS_PANEL_DATA,
};
return focusPanelStorage.value
? jsonParse(focusPanelStorage.value, { fallbackValue: defaultValue })
: defaultValue;
});
const currentFocusPanelData = computed(
(): FocusPanelData =>
focusPanelData.value[workflowsStore.workflowId] ?? DEFAULT_FOCUS_PANEL_DATA,
);
const focusPanelActive = computed(() => currentFocusPanelData.value.isActive);
const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters);
// An unenriched parameter indicates a missing nodeId
const focusedNodeParameters = computed<Array<RichFocusedNodeParameter | FocusedNodeParameter>>(
@@ -38,40 +70,76 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
}),
);
const setFocusedNodeParameter = (nodeParameter: FocusedNodeParameter) => {
_focusedNodeParameters.value = [
nodeParameter,
// Uncomment when tabs are implemented
// ...focusedNodeParameters.value.filter((p) => p.parameterPath !== nodeParameter.parameterPath),
];
const _setOptions = ({
parameters,
isActive,
wid = workflowsStore.workflowId,
removeEmpty = false,
}: {
isActive?: boolean;
parameters?: FocusedNodeParameter[];
wid?: string;
removeEmpty?: boolean;
}) => {
const focusPanelDataCurrent = focusPanelData.value;
if (removeEmpty && PLACEHOLDER_EMPTY_WORKFLOW_ID in focusPanelDataCurrent) {
delete focusPanelDataCurrent[PLACEHOLDER_EMPTY_WORKFLOW_ID];
}
focusPanelStorage.value = JSON.stringify({
...focusPanelData.value,
[wid]: {
isActive: isActive ?? focusPanelActive.value,
parameters: parameters ?? _focusedNodeParameters.value,
},
});
};
// When a new workflow is saved, we should update the focus panel data with the new workflow ID
const onNewWorkflowSave = (wid: string) => {
if (!currentFocusPanelData.value || !(PLACEHOLDER_EMPTY_WORKFLOW_ID in focusPanelData.value)) {
return;
}
const latestWorkflowData = focusPanelData.value[PLACEHOLDER_EMPTY_WORKFLOW_ID];
_setOptions({
wid,
parameters: latestWorkflowData.parameters,
isActive: latestWorkflowData.isActive,
removeEmpty: true,
});
};
const openWithFocusedNodeParameter = (nodeParameter: FocusedNodeParameter) => {
const parameters = [nodeParameter];
// TODO: uncomment when tabs are implemented
// ...focusedNodeParameters.value.filter((p) => p.parameterPath !== nodeParameter.parameterPath),
_setOptions({ parameters, isActive: true });
};
const closeFocusPanel = () => {
focusPanelActive.value = false;
_setOptions({ isActive: false });
};
const toggleFocusPanel = () => {
focusPanelActive.value = !focusPanelActive.value;
_setOptions({ isActive: !focusPanelActive.value });
};
const reset = () => {
focusPanelActive.value = false;
_focusedNodeParameters.value = [];
};
function isRichParameter(
const isRichParameter = (
p: RichFocusedNodeParameter | FocusedNodeParameter,
): p is RichFocusedNodeParameter {
): p is RichFocusedNodeParameter => {
return 'value' in p && 'node' in p;
}
};
return {
focusPanelActive,
focusedNodeParameters,
setFocusedNodeParameter,
openWithFocusedNodeParameter,
isRichParameter,
closeFocusPanel,
toggleFocusPanel,
reset,
onNewWorkflowSave,
};
});