mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Store focused panel state in local storage (no-changelog) (#17163)
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user