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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import { useTelemetry } from './useTelemetry';
import { useNodeHelpers } from './useNodeHelpers'; import { useNodeHelpers } from './useNodeHelpers';
import { tryToParseNumber } from '@/utils/typesUtils'; import { tryToParseNumber } from '@/utils/typesUtils';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { useFocusPanelStore } from '@/stores/focusPanel.store';
export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRouter> }) { export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRouter> }) {
const uiStore = useUIStore(); const uiStore = useUIStore();
@@ -33,6 +34,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
const message = useMessage(); const message = useMessage();
const i18n = useI18n(); const i18n = useI18n();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const focusPanelStore = useFocusPanelStore();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const toast = useToast(); const toast = useToast();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -346,6 +348,8 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
workflowsStore.addWorkflow(workflowData); workflowsStore.addWorkflow(workflowData);
focusPanelStore.onNewWorkflowSave(workflowData.id);
if (openInNewWindow) { if (openInNewWindow) {
const routeData = router.resolve({ const routeData = router.resolve({
name: VIEWS.WORKFLOW, 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_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_DISMISSED_WHATS_NEW_CALLOUT = 'N8N_DISMISSED_WHATS_NEW_CALLOUT';
export const LOCAL_STORAGE_NDV_PANEL_WIDTH = 'N8N_NDV_PANEL_WIDTH'; 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 BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
export const COMMUNITY_PLUS_DOCS_URL = export const COMMUNITY_PLUS_DOCS_URL =

View File

@@ -1,10 +1,17 @@
import { STORES } from '@n8n/stores'; import { STORES } from '@n8n/stores';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, ref } from 'vue'; import { computed } from 'vue';
import get from 'lodash/get'; 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 { useWorkflowsStore } from './workflows.store';
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { useStorage } from '@/composables/useStorage';
type FocusedNodeParameter = { type FocusedNodeParameter = {
nodeId: string; nodeId: string;
@@ -17,11 +24,36 @@ export type RichFocusedNodeParameter = FocusedNodeParameter & {
value: NodeParameterValueType; 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, () => { export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const focusPanelStorage = useStorage(LOCAL_STORAGE_FOCUS_PANEL);
const focusPanelActive = ref(false); const focusPanelData = computed((): FocusPanelDataByWid => {
const _focusedNodeParameters = ref<FocusedNodeParameter[]>([]); 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 // An unenriched parameter indicates a missing nodeId
const focusedNodeParameters = computed<Array<RichFocusedNodeParameter | FocusedNodeParameter>>( const focusedNodeParameters = computed<Array<RichFocusedNodeParameter | FocusedNodeParameter>>(
@@ -38,40 +70,76 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
}), }),
); );
const setFocusedNodeParameter = (nodeParameter: FocusedNodeParameter) => { const _setOptions = ({
_focusedNodeParameters.value = [ parameters,
nodeParameter, isActive,
// Uncomment when tabs are implemented wid = workflowsStore.workflowId,
// ...focusedNodeParameters.value.filter((p) => p.parameterPath !== nodeParameter.parameterPath), 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 = () => { const closeFocusPanel = () => {
focusPanelActive.value = false; _setOptions({ isActive: false });
}; };
const toggleFocusPanel = () => { const toggleFocusPanel = () => {
focusPanelActive.value = !focusPanelActive.value; _setOptions({ isActive: !focusPanelActive.value });
}; };
const reset = () => { const isRichParameter = (
focusPanelActive.value = false;
_focusedNodeParameters.value = [];
};
function isRichParameter(
p: RichFocusedNodeParameter | FocusedNodeParameter, p: RichFocusedNodeParameter | FocusedNodeParameter,
): p is RichFocusedNodeParameter { ): p is RichFocusedNodeParameter => {
return 'value' in p && 'node' in p; return 'value' in p && 'node' in p;
} };
return { return {
focusPanelActive, focusPanelActive,
focusedNodeParameters, focusedNodeParameters,
setFocusedNodeParameter, openWithFocusedNodeParameter,
isRichParameter, isRichParameter,
closeFocusPanel, closeFocusPanel,
toggleFocusPanel, toggleFocusPanel,
reset, onNewWorkflowSave,
}; };
}); });