refactor(editor): Unify template import initialization with the new workflow init path

This commit is contained in:
Ricardo Espinoza 2026-09-10 16:01:37 -04:00 committed by Svetoslav Dekov
parent 80731ca347
commit e1b73a2e36
6 changed files with 329 additions and 177 deletions

View File

@ -6858,14 +6858,11 @@ describe('useCanvasOperations', () => {
},
};
const getNewWorkflowData = vi.mocked(workflowsApi.getNewWorkflowData);
const setConnectionsSpy = vi.spyOn(workflowDocumentStoreInstance, 'setConnections');
const { importTemplate } = useCanvasOperations();
const templateName = 'template name';
await importTemplate({
name: templateName,
workflow,
});
@ -6882,11 +6879,6 @@ describe('useCanvasOperations', () => {
disabled: false,
});
expect(workflowDocumentStoreInstance.setNodePristine).toHaveBeenCalledWith(nodeB.name, true);
expect(getNewWorkflowData).toHaveBeenCalledWith(
expect.anything(),
templateName,
projectsStore.currentProjectId,
);
});
});
describe('replaceNodeParameters', () => {
@ -7785,6 +7777,7 @@ describe('useCanvasOperations', () => {
expect.anything(),
'Template Name',
'test-project-id',
undefined,
);
expect(telemetry.track).toHaveBeenCalledWith('User inserted workflow template', {
@ -7850,6 +7843,7 @@ describe('useCanvasOperations', () => {
expect.anything(),
'Template Name',
'test-project-id',
undefined,
);
expect(router.replace).toHaveBeenCalledWith({

View File

@ -34,6 +34,7 @@ import { getN8nAgentsNodeName } from '@/experiments/inlineAgents/useInlineAgents
import { type PinDataSource, usePinnedData } from '@/app/composables/usePinnedData';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import { useToast } from '@n8n/composables/useToast';
import { useNewWorkflowDocument } from '@/app/composables/useNewWorkflowDocument';
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
import { useWorkflowNormalization } from '@/app/composables/useWorkflowNormalization';
import { getExecutionErrorToastConfiguration } from '@/features/execution/executions/executions.utils';
@ -58,7 +59,6 @@ import {
ReplaceNodeParametersCommand,
UpdateNodeGroupCommand,
} from '@/app/models/history';
import * as workflowsApi from '@/app/api/workflows';
import { useCanvasStore } from '@/app/stores/canvas.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { getAutoSelectedCredential } from '@/features/credentials/credentials.utils';
@ -261,6 +261,7 @@ export function useCanvasOperations() {
const i18n = useI18n();
const toast = useToast();
const workflowHelpers = useWorkflowHelpers();
const { initializeNewWorkflowDocument } = useNewWorkflowDocument();
const nodeHelpers = useNodeHelpers();
const aiSimulatedDataGuard = useAiSimulatedDataGuard();
const {
@ -3585,10 +3586,8 @@ export function useCanvasOperations() {
}
async function importTemplate({
name,
workflow,
}: {
name?: string;
workflow: IWorkflowTemplate['workflow'] | WorkflowDataWithTemplateId;
}) {
let convertedNodes = workflow.nodes?.map(workflowsStore.convertTemplateNodeToNodeUi);
@ -3601,13 +3600,6 @@ export function useCanvasOperations() {
workflowDocumentStore.value.setConnections(workflow.connections);
}
await addNodes(convertedNodes ?? [], { keepPristine: true });
const workflowData = await workflowsApi.getNewWorkflowData(
rootStore.restApiContext,
name,
projectsStore.currentProjectId,
);
workflowDocumentStore.value.setName(workflowData.name);
workflowDocumentStore.value.setHydrated(true);
}
async function tryToOpenSubworkflowInNewTab(nodeId: string): Promise<boolean> {
@ -3817,7 +3809,8 @@ export function useCanvasOperations() {
workflowsStore.setWorkflowId(route.params.workflowId);
}
await importTemplate({ name: data.name, workflow: data.workflow });
await initializeNewWorkflowDocument({ name: data.name });
await importTemplate({ workflow: data.workflow });
workflowDocumentStore.value.addToMeta({ templateId: `${templateId}` });
@ -3870,10 +3863,8 @@ export function useCanvasOperations() {
workflowsStore.setWorkflowId(route.params.workflowId);
}
await importTemplate({
name: workflow.name,
workflow,
});
await initializeNewWorkflowDocument({ name: workflow.name, parentFolderId });
await importTemplate({ workflow });
workflowDocumentStore.value.addToMeta({ templateId: `${templateId}` });

View File

@ -0,0 +1,216 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { shallowRef, defineComponent, h } from 'vue';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { render } from '@testing-library/vue';
import { useNewWorkflowDocument } from './useNewWorkflowDocument';
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
import { useUIStore } from '@/app/stores/ui.store';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import type { Project } from '@/features/collaboration/projects/projects.types';
import { mockedStore } from '@/__tests__/utils';
const mockSetDocumentTitle = vi.hoisted(() => vi.fn());
vi.mock('@/app/composables/useDocumentTitle', () => ({
useDocumentTitle: vi.fn(() => ({
setDocumentTitle: mockSetDocumentTitle,
})),
}));
const mockGetNewWorkflowData = vi.hoisted(() =>
vi.fn().mockResolvedValue({ name: 'New Workflow', settings: {} }),
);
vi.mock('@/app/api/workflows', async (importOriginal) => ({
...(await importOriginal<typeof import('@/app/api/workflows')>()),
getNewWorkflowData: mockGetNewWorkflowData,
}));
const mockFetchParentFolder = vi.hoisted(() => vi.fn().mockResolvedValue(null));
vi.mock('@/features/core/folders/composables/useParentFolder', () => ({
useParentFolder: vi.fn(() => ({
fetchParentFolder: mockFetchParentFolder,
})),
}));
vi.mock('@/app/composables/useWorkflowId', async (importOriginal) => ({
...(await importOriginal<typeof import('@/app/composables/useWorkflowId')>()),
useWorkflowId: vi.fn(() => shallowRef('wf-1')),
}));
const mockRoute = vi.hoisted(() => ({
name: 'workflow' as string,
params: {} as Record<string, unknown>,
query: {} as Record<string, unknown>,
meta: {} as Record<string, unknown>,
}));
vi.mock('vue-router', async (importOriginal) => {
const actual = (await importOriginal()) as object;
return {
...actual,
useRoute: vi.fn(() => mockRoute),
useRouter: vi.fn(() => ({
replace: vi.fn().mockResolvedValue(undefined),
push: vi.fn().mockResolvedValue(undefined),
})),
};
});
const mockWorkflowDocumentStore = vi.hoisted(() => ({
workflowId: 'wf-1',
name: 'New Workflow',
setName: vi.fn(),
setHomeProject: vi.fn(),
setScopes: vi.fn(),
setParentFolder: vi.fn(),
setHydrated: vi.fn(),
onNameChange: vi.fn(),
}));
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => ({
...(await importOriginal<typeof import('@/app/stores/workflowDocument.store')>()),
useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn((id: string) => id),
}));
function renderWithComposable(
callback: (init: ReturnType<typeof useNewWorkflowDocument>) => void,
workflowDocumentStoreRef = shallowRef<typeof mockWorkflowDocumentStore | null>(
mockWorkflowDocumentStore,
),
) {
const TestComponent = defineComponent({
setup() {
callback(useNewWorkflowDocument());
return () => h('div');
},
});
return render(TestComponent, {
global: {
provide: {
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
},
},
});
}
describe('useNewWorkflowDocument', () => {
beforeEach(() => {
setActivePinia(createTestingPinia());
vi.clearAllMocks();
mockGetNewWorkflowData.mockResolvedValue({ name: 'New Workflow', settings: {} });
});
function initialize(options?: { name?: string; parentFolderId?: string }) {
let initializeNewWorkflowDocument!: ReturnType<
typeof useNewWorkflowDocument
>['initializeNewWorkflowDocument'];
renderWithComposable((composable) => {
initializeNewWorkflowDocument = composable.initializeNewWorkflowDocument;
});
return initializeNewWorkflowDocument(options);
}
it('sets the name from getNewWorkflowData and passes the requested name through', async () => {
mockGetNewWorkflowData.mockResolvedValue({ name: 'Template Name 2', settings: {} });
await initialize({ name: 'Template Name' });
expect(mockGetNewWorkflowData).toHaveBeenCalledWith(
expect.anything(),
'Template Name',
undefined,
undefined,
);
expect(mockWorkflowDocumentStore.setName).toHaveBeenCalledWith('Template Name 2');
expect(mockSetDocumentTitle).toHaveBeenCalledWith('Template Name 2', 'IDLE');
});
it('populates scopes and home project from the personal project', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update', 'workflow:publish'],
} as Project;
projectsStore.currentProject = null;
await initialize();
expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'personal-project-id' }),
);
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith([
'workflow:update',
'workflow:publish',
]);
expect(mockWorkflowDocumentStore.setHydrated).toHaveBeenCalledWith(true);
});
it('prefers the project that refreshCurrentProject resolves', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:read'],
} as Project;
projectsStore.currentProject = null;
projectsStore.refreshCurrentProject.mockImplementation(async () => {
projectsStore.currentProject = {
id: 'team-project-id',
scopes: ['workflow:update'],
} as Project;
});
await initialize();
expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'team-project-id' }),
);
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']);
});
it('falls back to the loaded projects when the project refresh fails', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update'],
} as Project;
projectsStore.currentProject = null;
projectsStore.refreshCurrentProject.mockRejectedValue(new Error('network error'));
await initialize();
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']);
});
it('does not populate a store that was replaced during the project refresh', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update'],
} as Project;
const storeRef = shallowRef<typeof mockWorkflowDocumentStore | null>(mockWorkflowDocumentStore);
projectsStore.refreshCurrentProject.mockImplementation(async () => {
storeRef.value = null;
});
let initializeNewWorkflowDocument!: ReturnType<
typeof useNewWorkflowDocument
>['initializeNewWorkflowDocument'];
renderWithComposable((composable) => {
initializeNewWorkflowDocument = composable.initializeNewWorkflowDocument;
}, storeRef);
await initializeNewWorkflowDocument();
expect(mockWorkflowDocumentStore.setHomeProject).not.toHaveBeenCalled();
expect(mockWorkflowDocumentStore.setScopes).not.toHaveBeenCalled();
});
it('marks the node view initialized', async () => {
const uiStore = mockedStore(useUIStore);
uiStore.nodeViewInitialized = false;
await initialize();
expect(uiStore.nodeViewInitialized).toBe(true);
});
});

View File

@ -0,0 +1,98 @@
import { hasInjectionContext, inject } from 'vue';
import * as workflowsApi from '@/app/api/workflows';
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
import { useWorkflowId } from '@/app/composables/useWorkflowId';
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
type WorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useParentFolder } from '@/features/core/folders/composables/useParentFolder';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { useRootStore } from '@n8n/stores/useRootStore';
/**
* Populates a freshly created workflow document store with everything the
* editor derives state from: name, home project, scopes, parent folder,
* hydration, and the name → list cache sync.
*
* The new-workflow path and the template import path must both run this, so
* a side effect added here reaches both. ADO-5844 came from the template
* path hand-copying only part of this list.
*/
export function useNewWorkflowDocument() {
const documentTitle = useDocumentTitle();
const rootStore = useRootStore();
const uiStore = useUIStore();
const workflowsListStore = useWorkflowsListStore();
const projectsStore = useProjectsStore();
const workflowId = useWorkflowId();
const { fetchParentFolder } = useParentFolder();
// The app-level writable ref (provided by App.vue). Layouts gate rendering
// on it, so it must point at the store this initialization creates.
const currentWorkflowDocumentStore = hasInjectionContext()
? inject(WorkflowDocumentStoreKey, null)
: null;
async function initializeNewWorkflowDocument({
name,
parentFolderId,
}: { name?: string; parentFolderId?: string } = {}): Promise<WorkflowDocumentStore> {
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowId.value),
);
if (currentWorkflowDocumentStore) {
currentWorkflowDocumentStore.value = workflowDocumentStore;
}
// Sync document store name → list cache (mirrors initializeWorkflowDocument)
workflowDocumentStore.onNameChange(({ payload }) => {
workflowsListStore.updateWorkflowInCache(workflowId.value, { name: payload.name });
});
const workflowData = await workflowsApi.getNewWorkflowData(
rootStore.restApiContext,
name,
projectsStore.currentProjectId,
parentFolderId,
);
workflowDocumentStore.setName(workflowData.name);
documentTitle.setDocumentTitle(workflowData.name, 'IDLE');
try {
await projectsStore.refreshCurrentProject();
} catch (error) {
// A stale project is recoverable; a rejection here would strand the
// caller's loading state.
console.error('Failed to refresh current project for a new workflow document', { error });
}
// Navigation during the async work can dispose or replace the store.
// Only populate the store this call created.
if (
currentWorkflowDocumentStore &&
currentWorkflowDocumentStore.value !== workflowDocumentStore
) {
return workflowDocumentStore;
}
const { currentProject, personalProject } = projectsStore;
// Must read the project after the refresh: a `?projectId=` deep link isn't fetched
// into the store until then, so an earlier read stamps personal as the owner.
workflowDocumentStore.setHomeProject(currentProject ?? personalProject ?? null);
workflowDocumentStore.setScopes(currentProject?.scopes ?? personalProject?.scopes ?? []);
const parentFolder = await fetchParentFolder(parentFolderId);
workflowDocumentStore.setParentFolder(parentFolder);
workflowDocumentStore.setHydrated(true);
uiStore.nodeViewInitialized = true;
return workflowDocumentStore;
}
return { initializeNewWorkflowDocument };
}

View File

@ -143,9 +143,6 @@ vi.mock('vue-router', async (importOriginal) => {
function renderWithComposable(
callback: (init: ReturnType<typeof useWorkflowInitialization>) => void,
workflowDocumentStoreRef = shallowRef<typeof mockWorkflowDocumentStore | null>(
mockWorkflowDocumentStore,
),
) {
const TestComponent = defineComponent({
setup() {
@ -158,7 +155,7 @@ function renderWithComposable(
return render(TestComponent, {
global: {
provide: {
[WorkflowDocumentStoreKey as symbol]: workflowDocumentStoreRef,
[WorkflowDocumentStoreKey as symbol]: shallowRef(mockWorkflowDocumentStore),
},
},
});
@ -262,8 +259,8 @@ describe('useWorkflowInitialization', () => {
});
});
describe('template import permissions', () => {
function importTemplate() {
describe('template import route', () => {
it('delegates the import to openWorkflowTemplate and reports it handled', async () => {
mockRoute.name = VIEWS.TEMPLATE_IMPORT;
mockRoute.params = { id: '11204' };
@ -271,91 +268,10 @@ describe('useWorkflowInitialization', () => {
renderWithComposable((init) => {
handleTemplateImportRoute = init.handleTemplateImportRoute;
});
return handleTemplateImportRoute();
}
it('populates scopes and home project on the document store', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update', 'workflow:publish'],
} as Project;
projectsStore.currentProject = null;
const handled = await importTemplate();
const handled = await handleTemplateImportRoute();
expect(handled).toBe(true);
expect(mockOpenWorkflowTemplate).toHaveBeenCalledWith('11204');
expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'personal-project-id' }),
);
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith([
'workflow:update',
'workflow:publish',
]);
});
it('prefers the project that refreshCurrentProject resolves', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:read'],
} as Project;
projectsStore.currentProject = null;
projectsStore.refreshCurrentProject.mockImplementation(async () => {
projectsStore.currentProject = {
id: 'team-project-id',
scopes: ['workflow:update'],
} as Project;
});
await importTemplate();
expect(mockWorkflowDocumentStore.setHomeProject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'team-project-id' }),
);
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']);
});
it('falls back to the loaded projects when the project refresh fails', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update'],
} as Project;
projectsStore.currentProject = null;
projectsStore.refreshCurrentProject.mockRejectedValue(new Error('network error'));
const handled = await importTemplate();
expect(handled).toBe(true);
expect(mockWorkflowDocumentStore.setScopes).toHaveBeenCalledWith(['workflow:update']);
});
it('does not stamp a store that was replaced during the project refresh', async () => {
mockRoute.name = VIEWS.TEMPLATE_IMPORT;
mockRoute.params = { id: '11204' };
const projectsStore = mockedStore(useProjectsStore);
projectsStore.personalProject = {
id: 'personal-project-id',
scopes: ['workflow:update'],
} as Project;
const storeRef = shallowRef<typeof mockWorkflowDocumentStore | null>(
mockWorkflowDocumentStore,
);
projectsStore.refreshCurrentProject.mockImplementation(async () => {
storeRef.value = null;
});
let handleTemplateImportRoute!: () => Promise<boolean>;
renderWithComposable((init) => {
handleTemplateImportRoute = init.handleTemplateImportRoute;
}, storeRef);
await handleTemplateImportRoute();
expect(mockWorkflowDocumentStore.setHomeProject).not.toHaveBeenCalled();
expect(mockWorkflowDocumentStore.setScopes).not.toHaveBeenCalled();
});
});

View File

@ -6,9 +6,7 @@ import { useToast } from '@n8n/composables/useToast';
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { useParentFolder } from '@/features/core/folders/composables/useParentFolder';
import * as workflowsApi from '@/app/api/workflows';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNewWorkflowDocument } from '@/app/composables/useNewWorkflowDocument';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
@ -46,7 +44,6 @@ export function useWorkflowInitialization() {
const documentTitle = useDocumentTitle();
const externalHooks = useExternalHooks();
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const workflowsListStore = useWorkflowsListStore();
const uiStore = useUIStore();
@ -72,13 +69,12 @@ export function useWorkflowInitialization() {
openWorkflowTemplate,
openWorkflowTemplateFromJSON,
} = useCanvasOperations();
const { initializeNewWorkflowDocument } = useNewWorkflowDocument();
const { applyExecutionData } = useExecutionDebugging();
const isLoading = ref(true);
const initializedWorkflowId = ref<string | undefined>();
const { fetchParentFolder } = useParentFolder();
function disposeCurrentWorkflowDocumentStore() {
const workflowDocumentStore = currentWorkflowDocumentStore.value;
@ -170,35 +166,6 @@ export function useWorkflowInitialization() {
await openWorkflowTemplate(templateId.toString());
}
// Create document store for template workflow (empty tags initially)
// The workflow ID was set during the template import
const currentWorkflowId = workflowId.value;
if (currentWorkflowId) {
const workflowDocumentId = createWorkflowDocumentId(currentWorkflowId);
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
currentWorkflowDocumentStore.value = workflowDocumentStore;
documentTitle.setDocumentTitle(workflowDocumentStore.name, 'IDLE');
// The header derives every permission from the document store scopes.
// Without them, the first save turns the whole header read-only
// (Publish and Save hidden, actions menu disabled).
try {
await projectsStore.refreshCurrentProject();
} catch (error) {
// A stale project is recoverable; a rejection here would leave the
// route handler's caller with isLoading stuck on the loading view.
console.error('Failed to refresh current project during template import', { error });
}
// Navigation during the refresh can dispose or replace the store.
// Only stamp the store this import created.
if (currentWorkflowDocumentStore.value === workflowDocumentStore) {
const { currentProject, personalProject } = projectsStore;
workflowDocumentStore.setHomeProject(currentProject ?? personalProject ?? null);
workflowDocumentStore.setScopes(currentProject?.scopes ?? personalProject?.scopes ?? []);
}
}
return true;
}
@ -327,38 +294,8 @@ export function useWorkflowInitialization() {
workflowsStore.setWorkflowId(workflowId.value);
const workflowDocumentId = createWorkflowDocumentId(workflowId.value);
currentWorkflowDocumentStore.value = useWorkflowDocumentStore(workflowDocumentId);
await initializeNewWorkflowDocument({ parentFolderId });
// Sync document store name → list cache (mirrors initializeWorkflowDocument)
currentWorkflowDocumentStore.value.onNameChange(({ payload }) => {
workflowsListStore.updateWorkflowInCache(workflowId.value, { name: payload.name });
});
const workflowData = await workflowsApi.getNewWorkflowData(
rootStore.restApiContext,
undefined,
projectsStore.currentProjectId,
parentFolderId,
);
currentWorkflowDocumentStore.value.setName(workflowData.name);
documentTitle.setDocumentTitle(workflowData.name, 'IDLE');
await projectsStore.refreshCurrentProject();
const { currentProject, personalProject } = projectsStore;
// Must read the project after the refresh: a `?projectId=` deep link isn't fetched
// into the store until then, so an earlier read stamps personal as the owner.
currentWorkflowDocumentStore.value.setHomeProject(currentProject ?? personalProject ?? null);
currentWorkflowDocumentStore.value.setScopes(
currentProject?.scopes ?? personalProject?.scopes ?? [],
);
const parentFolder = await fetchParentFolder(parentFolderId);
currentWorkflowDocumentStore.value?.setParentFolder(parentFolder);
currentWorkflowDocumentStore.value.setHydrated(true);
uiStore.nodeViewInitialized = true;
initializedWorkflowId.value = workflowId.value;
fitView();