refactor(editor): Normalize sharedWithProjects field in workflow document store (no-changelog) (#28078)

This commit is contained in:
Alex Grozav 2026-04-20 11:50:56 +03:00 committed by GitHub
parent 0fc2d90b52
commit d037fd4647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 27 deletions

View file

@ -78,7 +78,8 @@ const workflowScopes = computed(
workflowsStore.workflow.scopes,
);
const workflowSharedWithProjects = computed(
() => workflowListEntry.value?.sharedWithProjects ?? workflowsStore.workflow.sharedWithProjects,
() =>
workflowDocumentStore.value?.sharedWithProjects ?? workflowListEntry.value?.sharedWithProjects,
);
const loading = ref(true);
const isDirty = ref(false);
@ -203,6 +204,9 @@ const onSave = async () => {
workflowId,
sharedWithProjects: sharedWithProjects.value,
});
useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)).setSharedWithProjects(
sharedWithProjects.value,
);
toast.showMessage({
title: i18n.baseText('workflows.shareModal.onSave.success.title'),

View file

@ -6,7 +6,6 @@ import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useWorkflowsEEStore } from '@/app/stores/workflows.ee.store';
import { useTagsStore } from '@/features/shared/tags/tags.store';
import { useUIStore } from '@/app/stores/ui.store';
import {
@ -51,7 +50,6 @@ describe('useWorkflowHelpers', () => {
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let workflowsListStore: ReturnType<typeof mockedStore<typeof useWorkflowsListStore>>;
let workflowState: WorkflowState;
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
let tagsStore: ReturnType<typeof useTagsStore>;
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
@ -63,7 +61,6 @@ describe('useWorkflowHelpers', () => {
workflowState = useWorkflowState();
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowsEEStore = useWorkflowsEEStore();
tagsStore = useTagsStore();
uiStore = mockedStore(useUIStore);
});
@ -247,14 +244,9 @@ describe('useWorkflowHelpers', () => {
});
const addWorkflowSpy = vi.spyOn(workflowsListStore, 'addWorkflow');
const setWorkflowIdSpy = vi.spyOn(workflowState, 'setWorkflowId');
const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith');
const upsertTagsSpy = vi.spyOn(tagsStore, 'upsertTags');
await initState(workflowData);
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowData.id),
);
const { workflowDocumentStore } = await initState(workflowData);
expect(addWorkflowSpy).toHaveBeenCalledWith(workflowData);
expect(setWorkflowIdSpy).toHaveBeenCalledWith('1');
@ -266,10 +258,8 @@ describe('useWorkflowHelpers', () => {
name: null,
description: null,
});
expect(setWorkflowSharedWithSpy).toHaveBeenCalledWith({
workflowId: '1',
sharedWithProjects: [],
});
// sharedWithProjects is now managed by workflowDocumentStore
expect(workflowDocumentStore.setSharedWithProjects).toHaveBeenCalledWith([]);
// Tags are now managed by workflowDocumentStore
expect(upsertTagsSpy).toHaveBeenCalledWith([]);
});
@ -286,11 +276,11 @@ describe('useWorkflowHelpers', () => {
scopes: [],
tags: [],
});
const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith');
await initState(workflowData);
const { workflowDocumentStore } = await initState(workflowData);
expect(setWorkflowSharedWithSpy).not.toHaveBeenCalled();
// When sharedWithProjects is undefined, it defaults to empty array
expect(workflowDocumentStore.setSharedWithProjects).toHaveBeenCalledWith([]);
});
it('should handle missing `tags` gracefully', async () => {

View file

@ -54,7 +54,6 @@ import { convertWorkflowTagsToIds } from '@/app/utils/workflowUtils';
import { useI18n } from '@n8n/i18n';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { useTagsStore } from '@/features/shared/tags/tags.store';
import { useWorkflowsEEStore } from '@/app/stores/workflows.ee.store';
import { findWebhook } from '@n8n/rest-api-client/api/webhooks';
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
@ -505,7 +504,6 @@ export function useWorkflowHelpers() {
const workflowsStore = useWorkflowsStore();
const workflowsListStore = useWorkflowsListStore();
const workflowState = injectWorkflowState();
const workflowsEEStore = useWorkflowsEEStore();
const uiStore = useUIStore();
const nodeHelpers = useNodeHelpers();
const projectsStore = useProjectsStore();
@ -938,12 +936,13 @@ export function useWorkflowHelpers() {
function getWorkflowProjectRole(workflowId: string): 'owner' | 'sharee' | 'member' {
const workflow = workflowsListStore.getWorkflowById(workflowId);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
// Check if workflow is new (not saved) or belongs to personal project
if (workflow?.homeProject?.id === projectsStore.personalProject?.id || !workflow?.id) {
return 'owner';
} else if (
workflow?.sharedWithProjects?.some(
workflowDocumentStore.sharedWithProjects?.some(
(project) => project.id === projectsStore.personalProject?.id,
)
) {
@ -989,13 +988,6 @@ export function useWorkflowHelpers() {
}
}
if (workflowData.sharedWithProjects) {
workflowsEEStore.setWorkflowSharedWith({
workflowId: workflowData.id,
sharedWithProjects: workflowData.sharedWithProjects,
});
}
const tags = (workflowData.tags ?? []) as ITag[];
const tagIds = convertWorkflowTagsToIds(tags);
@ -1032,6 +1024,7 @@ export function useWorkflowHelpers() {
initializedWorkflowDocumentStore.setMeta(workflowData.meta);
initializedWorkflowDocumentStore.setParentFolder(workflowData.parentFolder ?? null);
initializedWorkflowDocumentStore.setScopes(workflowData.scopes ?? []);
initializedWorkflowDocumentStore.setSharedWithProjects(workflowData.sharedWithProjects ?? []);
initializedWorkflowDocumentStore.setDescription(workflowData.description);
tagsStore.upsertTags(tags);

View file

@ -4,6 +4,7 @@ import { inject } from 'vue';
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
import { useWorkflowDocumentActive } from './workflowDocument/useWorkflowDocumentActive';
import { useWorkflowDocumentHomeProject } from './workflowDocument/useWorkflowDocumentHomeProject';
import { useWorkflowDocumentSharedWithProjects } from './workflowDocument/useWorkflowDocumentSharedWithProjects';
import { useWorkflowDocumentChecksum } from './workflowDocument/useWorkflowDocumentChecksum';
import { useWorkflowDocumentDescription } from './workflowDocument/useWorkflowDocumentDescription';
import { useWorkflowDocumentMeta } from './workflowDocument/useWorkflowDocumentMeta';
@ -115,6 +116,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
const workflowDocumentName = useWorkflowDocumentName();
const workflowDocumentActive = useWorkflowDocumentActive();
const workflowDocumentHomeProject = useWorkflowDocumentHomeProject();
const workflowDocumentSharedWithProjects = useWorkflowDocumentSharedWithProjects();
const workflowDocumentChecksum = useWorkflowDocumentChecksum();
const workflowDocumentDescription = useWorkflowDocumentDescription();
const workflowDocumentMeta = useWorkflowDocumentMeta();
@ -161,6 +163,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
...workflowDocumentName,
...workflowDocumentActive,
...workflowDocumentHomeProject,
...workflowDocumentSharedWithProjects,
...workflowDocumentChecksum,
...workflowDocumentDescription,
...workflowDocumentIsArchived,

View file

@ -0,0 +1,65 @@
import { describe, it, expect, vi } from 'vitest';
import { useWorkflowDocumentSharedWithProjects } from './useWorkflowDocumentSharedWithProjects';
import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types';
function createSharedWithProjects() {
return useWorkflowDocumentSharedWithProjects();
}
function createProject(overrides: Partial<ProjectSharingData> = {}): ProjectSharingData {
return {
id: 'project-1',
name: 'Test Project',
icon: null,
type: 'team',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
...overrides,
};
}
describe('useWorkflowDocumentSharedWithProjects', () => {
describe('initial state', () => {
it('should start with null', () => {
const { sharedWithProjects } = createSharedWithProjects();
expect(sharedWithProjects.value).toBeNull();
});
});
describe('setSharedWithProjects', () => {
it('should set projects and fire event hook', () => {
const { sharedWithProjects, setSharedWithProjects, onSharedWithProjectsChange } =
createSharedWithProjects();
const hookSpy = vi.fn();
onSharedWithProjectsChange(hookSpy);
const projects = [createProject({ id: 'p1' }), createProject({ id: 'p2' })];
setSharedWithProjects(projects);
expect(sharedWithProjects.value).toEqual(projects);
expect(hookSpy).toHaveBeenCalledWith({
action: 'update',
payload: { sharedWithProjects: projects },
});
});
it('should replace existing projects entirely', () => {
const { sharedWithProjects, setSharedWithProjects } = createSharedWithProjects();
setSharedWithProjects([createProject({ id: 'p1' })]);
const newProjects = [createProject({ id: 'p2' }), createProject({ id: 'p3' })];
setSharedWithProjects(newProjects);
expect(sharedWithProjects.value).toEqual(newProjects);
});
it('should allow setting empty array', () => {
const { sharedWithProjects, setSharedWithProjects } = createSharedWithProjects();
setSharedWithProjects([createProject({ id: 'p1' })]);
setSharedWithProjects([]);
expect(sharedWithProjects.value).toEqual([]);
});
});
});

View file

@ -0,0 +1,38 @@
import { ref, readonly } from 'vue';
import { createEventHook } from '@vueuse/core';
import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types';
import { CHANGE_ACTION } from './types';
import type { ChangeAction, ChangeEvent } from './types';
export type SharedWithProjectsPayload = {
sharedWithProjects: ProjectSharingData[];
};
export type SharedWithProjectsChangeEvent = ChangeEvent<SharedWithProjectsPayload>;
export function useWorkflowDocumentSharedWithProjects() {
const sharedWithProjects = ref<ProjectSharingData[] | null>(null);
const onSharedWithProjectsChange = createEventHook<SharedWithProjectsChangeEvent>();
function applySharedWithProjects(
projects: ProjectSharingData[],
action: ChangeAction = CHANGE_ACTION.UPDATE,
) {
sharedWithProjects.value = projects;
void onSharedWithProjectsChange.trigger({
action,
payload: { sharedWithProjects: projects },
});
}
function setSharedWithProjects(projects: ProjectSharingData[]) {
applySharedWithProjects(projects);
}
return {
sharedWithProjects: readonly(sharedWithProjects),
setSharedWithProjects,
onSharedWithProjectsChange: onSharedWithProjectsChange.on,
};
}