mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(editor): WorkflowsView to script setup + tests (#11078)
This commit is contained in:
parent
e081fd1f0b
commit
ce69218f1b
|
@ -1,111 +1,299 @@
|
||||||
import { afterAll, beforeAll } from 'vitest';
|
|
||||||
import { setActivePinia, createPinia } from 'pinia';
|
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { setupServer } from '@/__tests__/server';
|
|
||||||
import WorkflowsView from '@/views/WorkflowsView.vue';
|
import WorkflowsView from '@/views/WorkflowsView.vue';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { STORES, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import type { Cloud, IUser, IWorkflowDb } from '@/Interface';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import type { Project } from '@/types/projects.types';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import * as usersApi from '@/api/users';
|
||||||
|
|
||||||
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
vi.mock('@/api/projects.api');
|
||||||
HTMLElement.prototype,
|
vi.mock('@/api/users');
|
||||||
'offsetHeight',
|
vi.mock('@/api/sourceControl');
|
||||||
) as PropertyDescriptor;
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/:projectId?',
|
||||||
|
component: { template: '<div></div>' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workflow',
|
||||||
|
name: VIEWS.NEW_WORKFLOW,
|
||||||
|
component: { template: '<div></div>' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(WorkflowsView, {
|
||||||
|
global: {
|
||||||
|
plugins: [router],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
[STORES.SETTINGS]: { settings: { enterprise: { sharing: false } } },
|
||||||
|
};
|
||||||
|
|
||||||
describe('WorkflowsView', () => {
|
describe('WorkflowsView', () => {
|
||||||
let server: ReturnType<typeof setupServer>;
|
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
|
||||||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
|
||||||
|
|
||||||
const routerReplaceMock = vi.fn();
|
|
||||||
const routerPushMock = vi.fn();
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(WorkflowsView, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
$route: {
|
|
||||||
query: {},
|
|
||||||
params: {},
|
|
||||||
},
|
|
||||||
$router: {
|
|
||||||
replace: routerReplaceMock,
|
|
||||||
push: routerPushMock,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Object.defineProperties(HTMLElement.prototype, {
|
|
||||||
offsetHeight: {
|
|
||||||
get() {
|
|
||||||
return this.getAttribute('data-test-id') === 'resources-list' ? 1000 : 100;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
server = setupServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
pinia = createPinia();
|
await router.push('/');
|
||||||
setActivePinia(pinia);
|
await router.isReady();
|
||||||
|
|
||||||
settingsStore = useSettingsStore();
|
|
||||||
usersStore = useUsersStore();
|
|
||||||
projectsStore = useProjectsStore();
|
|
||||||
|
|
||||||
vi.spyOn(projectsStore, 'getAvailableProjects').mockImplementation(async () => {});
|
|
||||||
|
|
||||||
await settingsStore.getSettings();
|
|
||||||
await usersStore.fetchUsers();
|
|
||||||
await usersStore.loginWithCookie();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
describe('should show empty state', () => {
|
||||||
server.shutdown();
|
it('for non setup user', () => {
|
||||||
|
const { getByRole } = renderComponent({ pinia: createTestingPinia({ initialState }) });
|
||||||
|
expect(getByRole('heading').textContent).toBe('👋 Welcome!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for currentUser user', () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const userStore = mockedStore(useUsersStore);
|
||||||
|
userStore.currentUser = { firstName: 'John' } as IUser;
|
||||||
|
const { getByRole } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(getByRole('heading').textContent).toBe('👋 Welcome John!');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when onboardingExperiment -> False', () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const posthog = mockedStore(usePostHog);
|
||||||
|
const sourceControl = mockedStore(useSourceControlStore);
|
||||||
|
posthog.getVariant.mockReturnValue(MORE_ONBOARDING_OPTIONS_EXPERIMENT.control);
|
||||||
|
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
|
||||||
|
it('for readOnlyEnvironment', () => {
|
||||||
|
sourceControl.preferences.branchReadOnly = true;
|
||||||
|
|
||||||
|
const { getByText } = renderComponent({ pinia });
|
||||||
|
expect(getByText('No workflows here yet')).toBeInTheDocument();
|
||||||
|
sourceControl.preferences.branchReadOnly = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for noPermission', () => {
|
||||||
|
const { getByText } = renderComponent({ pinia });
|
||||||
|
expect(getByText('There are currently no workflows to view')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for user with create scope', () => {
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
const { getByText } = renderComponent({ pinia });
|
||||||
|
expect(getByText('Create your first workflow')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow workflow creation', async () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
const { getByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(getByTestId('new-workflow-card')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('new-workflow-card'));
|
||||||
|
|
||||||
|
expect(router.currentRoute.value.name).toBe(VIEWS.NEW_WORKFLOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should show courses and templates link for sales users', () => {
|
||||||
|
it('for cloudUser', () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const userStore = mockedStore(useUsersStore);
|
||||||
|
userStore.currentUserCloudInfo = { role: 'Sales' } as Cloud.UserAccount;
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for personalizationAnswers', () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const userStore = mockedStore(useUsersStore);
|
||||||
|
userStore.currentUser = { personalizationAnswers: { role: 'Sales' } } as IUser;
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show courses and templates link for onboardingExperiment', () => {
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
|
||||||
|
const posthog = mockedStore(usePostHog);
|
||||||
|
posthog.getVariant.mockReturnValue(MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant);
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter workflows by tags', async () => {
|
describe('workflow creation button', () => {
|
||||||
const { getByTestId, getAllByTestId, queryByTestId } = renderComponent({
|
it('should create global workflow', async () => {
|
||||||
pinia,
|
const pushSpy = vi.spyOn(router, 'push');
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
workflowsStore.allWorkflows = [{ id: '1' } as IWorkflowDb];
|
||||||
|
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
projectsStore.fetchProject.mockResolvedValue({} as Project);
|
||||||
|
projectsStore.personalProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({ pinia });
|
||||||
|
expect(getByTestId('resources-list-add')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(getByTestId('resources-list-add').textContent).toBe('Add workflow');
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('resources-list-add'));
|
||||||
|
|
||||||
|
expect(pushSpy).toHaveBeenCalledWith({ name: VIEWS.NEW_WORKFLOW, query: { projectId: '' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(queryByTestId('resources-list')).not.toBeInTheDocument();
|
it('should create a project specific workflow', async () => {
|
||||||
|
await router.replace({ path: '/project-id' });
|
||||||
|
const pushSpy = vi.spyOn(router, 'push');
|
||||||
|
|
||||||
await waitFor(() => {
|
const pinia = createTestingPinia({ initialState });
|
||||||
// There are 5 workflows defined in server fixtures
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
expect(getAllByTestId('resources-list-item')).toHaveLength(5);
|
workflowsStore.allWorkflows = [{ id: '1' } as IWorkflowDb];
|
||||||
|
|
||||||
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
|
||||||
|
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({ pinia });
|
||||||
|
expect(router.currentRoute.value.params.projectId).toBe('project-id');
|
||||||
|
|
||||||
|
expect(getByTestId('resources-list-add')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(getByTestId('resources-list-add').textContent).toBe('Add workflow to project');
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('resources-list-add'));
|
||||||
|
|
||||||
|
expect(pushSpy).toHaveBeenCalledWith({
|
||||||
|
name: VIEWS.NEW_WORKFLOW,
|
||||||
|
query: { projectId: 'project-id' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filters', () => {
|
||||||
|
it('should set tag filter based on query parameters', async () => {
|
||||||
|
await router.replace({ query: { tags: 'test-tag' } });
|
||||||
|
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const tagStore = mockedStore(useTagsStore);
|
||||||
|
tagStore.allTags = [{ id: 'test-tag', name: 'tag' }];
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
workflowsStore.allWorkflows = [
|
||||||
|
{ id: '1' },
|
||||||
|
{ id: '2', tags: [{ id: 'test-tag', name: 'tag' }] },
|
||||||
|
] as IWorkflowDb[];
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(tagStore.fetchAll).toHaveBeenCalled();
|
||||||
|
await waitFor(() => expect(getAllByTestId('resources-list-item').length).toBe(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.click(
|
it('should set search filter based on query parameters', async () => {
|
||||||
getAllByTestId('resources-list-item')[0].querySelector('.n8n-tag') as HTMLElement,
|
await router.replace({ query: { search: 'one' } });
|
||||||
);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getAllByTestId('resources-list-item').length).toBeLessThan(5);
|
|
||||||
});
|
|
||||||
expect(routerReplaceMock).toHaveBeenLastCalledWith({ query: { tags: '1' } });
|
|
||||||
|
|
||||||
await userEvent.click(getByTestId('workflows-filter-reset'));
|
const pinia = createTestingPinia({ initialState });
|
||||||
await waitFor(() => {
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
expect(getAllByTestId('resources-list-item')).toHaveLength(5);
|
workflowsStore.allWorkflows = [
|
||||||
});
|
{ id: '1', name: 'one' },
|
||||||
expect(routerReplaceMock).toHaveBeenLastCalledWith({ query: undefined });
|
{ id: '2', name: 'two' },
|
||||||
|
] as IWorkflowDb[];
|
||||||
|
|
||||||
await userEvent.click(
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
getAllByTestId('resources-list-item')[3].querySelector('.n8n-tag') as HTMLElement,
|
|
||||||
);
|
await waitFor(() => expect(getAllByTestId('resources-list-item').length).toBe(1));
|
||||||
await waitFor(() => {
|
|
||||||
expect(getAllByTestId('resources-list-item').length).toBeLessThan(5);
|
|
||||||
});
|
});
|
||||||
expect(routerReplaceMock).toHaveBeenLastCalledWith({ query: { tags: '1' } });
|
|
||||||
|
it('should set status filter based on query parameters', async () => {
|
||||||
|
await router.replace({ query: { status: 'true' } });
|
||||||
|
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
workflowsStore.allWorkflows = [
|
||||||
|
{ id: '1', active: true },
|
||||||
|
{ id: '2', active: false },
|
||||||
|
] as IWorkflowDb[];
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
await waitFor(() => expect(getAllByTestId('resources-list-item').length).toBe(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset filters', async () => {
|
||||||
|
await router.replace({ query: { status: 'true' } });
|
||||||
|
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
workflowsStore.allWorkflows = [
|
||||||
|
{ id: '1', active: true },
|
||||||
|
{ id: '2', active: false },
|
||||||
|
] as IWorkflowDb[];
|
||||||
|
|
||||||
|
const { getAllByTestId, getByTestId } = renderComponent({ pinia });
|
||||||
|
|
||||||
|
await waitFor(() => expect(getAllByTestId('resources-list-item').length).toBe(1));
|
||||||
|
await waitFor(() => expect(getByTestId('workflows-filter-reset')).toBeInTheDocument());
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('workflows-filter-reset'));
|
||||||
|
await waitFor(() => expect(getAllByTestId('resources-list-item').length).toBe(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove incomplete properties', async () => {
|
||||||
|
await router.replace({ query: { tags: '' } });
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
renderComponent({ pinia });
|
||||||
|
await waitFor(() => expect(router.currentRoute.value.query).toStrictEqual({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove invalid tabs', async () => {
|
||||||
|
await router.replace({ query: { tags: 'non-existing-tag' } });
|
||||||
|
const tagStore = mockedStore(useTagsStore);
|
||||||
|
tagStore.allTags = [{ id: 'test-tag', name: 'tag' }];
|
||||||
|
const pinia = createTestingPinia({ initialState });
|
||||||
|
renderComponent({ pinia });
|
||||||
|
await waitFor(() => expect(router.currentRoute.value.query).toStrictEqual({}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reinitialize on source control pullWorkfolder', async () => {
|
||||||
|
vi.spyOn(usersApi, 'getUsers').mockResolvedValue([]);
|
||||||
|
const pinia = createTestingPinia({ initialState, stubActions: false });
|
||||||
|
const userStore = mockedStore(useUsersStore);
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
workflowsStore.fetchAllWorkflows.mockResolvedValue([]);
|
||||||
|
workflowsStore.fetchActiveWorkflows.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const sourceControl = useSourceControlStore();
|
||||||
|
|
||||||
|
renderComponent({ pinia });
|
||||||
|
|
||||||
|
expect(userStore.fetchUsers).toHaveBeenCalledTimes(1);
|
||||||
|
await sourceControl.pullWorkfolder(true);
|
||||||
|
expect(userStore.fetchUsers).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, onMounted, watch, ref } from 'vue';
|
||||||
import ResourcesListLayout, { type IResource } from '@/components/layouts/ResourcesListLayout.vue';
|
import ResourcesListLayout, { type IResource } from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
import WorkflowCard from '@/components/WorkflowCard.vue';
|
import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
|
import type { ITag, IUser, IWorkflowDb } from '@/Interface';
|
||||||
import { mapStores } from 'pinia';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
@ -18,6 +17,37 @@ import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import {
|
||||||
|
N8nButton,
|
||||||
|
N8nCard,
|
||||||
|
N8nHeading,
|
||||||
|
N8nIcon,
|
||||||
|
N8nInputLabel,
|
||||||
|
N8nOption,
|
||||||
|
N8nSelect,
|
||||||
|
N8nText,
|
||||||
|
N8nTooltip,
|
||||||
|
} from 'n8n-design-system';
|
||||||
|
import { pickBy } from 'lodash-es';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const sourceControlStore = useSourceControlStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const posthogStore = usePostHog();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const templatesStore = useTemplatesStore();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const tagsStore = useTagsStore();
|
||||||
|
const documentTitle = useDocumentTitle();
|
||||||
|
|
||||||
interface Filters {
|
interface Filters {
|
||||||
search: string;
|
search: string;
|
||||||
|
@ -26,308 +56,250 @@ interface Filters {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryFilters = Partial<Filters>;
|
|
||||||
|
|
||||||
const StatusFilter = {
|
const StatusFilter = {
|
||||||
ACTIVE: true,
|
ACTIVE: true,
|
||||||
DEACTIVATED: false,
|
DEACTIVATED: false,
|
||||||
ALL: '',
|
ALL: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkflowsView = defineComponent({
|
const loading = ref(false);
|
||||||
name: 'WorkflowsView',
|
const filters = ref<Filters>({
|
||||||
components: {
|
search: '',
|
||||||
ResourcesListLayout,
|
homeProject: '',
|
||||||
WorkflowCard,
|
status: StatusFilter.ALL,
|
||||||
WorkflowTagsDropdown,
|
tags: [],
|
||||||
ProjectTabs,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
search: '',
|
|
||||||
homeProject: '',
|
|
||||||
status: StatusFilter.ALL,
|
|
||||||
tags: [],
|
|
||||||
} as Filters,
|
|
||||||
sourceControlStoreUnsubscribe: () => {},
|
|
||||||
loading: false,
|
|
||||||
documentTitle: useDocumentTitle(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(
|
|
||||||
useSettingsStore,
|
|
||||||
useUIStore,
|
|
||||||
useUsersStore,
|
|
||||||
useWorkflowsStore,
|
|
||||||
useSourceControlStore,
|
|
||||||
useTagsStore,
|
|
||||||
useProjectsStore,
|
|
||||||
useTemplatesStore,
|
|
||||||
usePostHog,
|
|
||||||
),
|
|
||||||
readOnlyEnv(): boolean {
|
|
||||||
return this.sourceControlStore.preferences.branchReadOnly;
|
|
||||||
},
|
|
||||||
currentUser(): IUser {
|
|
||||||
return this.usersStore.currentUser || ({} as IUser);
|
|
||||||
},
|
|
||||||
allWorkflows(): IResource[] {
|
|
||||||
return this.workflowsStore.allWorkflows as IResource[];
|
|
||||||
},
|
|
||||||
isShareable(): boolean {
|
|
||||||
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
|
|
||||||
},
|
|
||||||
statusFilterOptions(): Array<{ label: string; value: string | boolean }> {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('workflows.filters.status.all'),
|
|
||||||
value: StatusFilter.ALL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('workflows.filters.status.active'),
|
|
||||||
value: StatusFilter.ACTIVE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.$locale.baseText('workflows.filters.status.deactivated'),
|
|
||||||
value: StatusFilter.DEACTIVATED,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
userRole() {
|
|
||||||
const role = this.usersStore.currentUserCloudInfo?.role;
|
|
||||||
|
|
||||||
if (role) {
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
|
|
||||||
const answers = this.usersStore.currentUser?.personalizationAnswers;
|
|
||||||
if (answers && 'role' in answers) {
|
|
||||||
return answers.role;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
isOnboardingExperimentEnabled() {
|
|
||||||
return (
|
|
||||||
this.posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isSalesUser() {
|
|
||||||
if (!this.userRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return ['Sales', 'sales-and-marketing'].includes(this.userRole);
|
|
||||||
},
|
|
||||||
addWorkflowButtonText() {
|
|
||||||
return this.projectsStore.currentProject
|
|
||||||
? this.$locale.baseText('workflows.project.add')
|
|
||||||
: this.$locale.baseText('workflows.add');
|
|
||||||
},
|
|
||||||
projectPermissions() {
|
|
||||||
return getResourcePermissions(
|
|
||||||
this.projectsStore.currentProject?.scopes ?? this.projectsStore.personalProject?.scopes,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
emptyListDescription() {
|
|
||||||
if (this.readOnlyEnv) {
|
|
||||||
return this.$locale.baseText('workflows.empty.description.readOnlyEnv');
|
|
||||||
} else if (!this.projectPermissions.workflow.create) {
|
|
||||||
return this.$locale.baseText('workflows.empty.description.noPermission');
|
|
||||||
} else {
|
|
||||||
return this.$locale.baseText('workflows.empty.description');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
filters: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.saveFiltersOnQueryString();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'$route.params.projectId'() {
|
|
||||||
void this.initialize();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.documentTitle.set(this.$locale.baseText('workflows.heading'));
|
|
||||||
|
|
||||||
await this.tagsStore.fetchAll();
|
|
||||||
|
|
||||||
await this.setFiltersFromQueryString();
|
|
||||||
|
|
||||||
void this.usersStore.showPersonalizationSurvey();
|
|
||||||
|
|
||||||
this.sourceControlStoreUnsubscribe = this.sourceControlStore.$onAction(({ name, after }) => {
|
|
||||||
if (name === 'pullWorkfolder' && after) {
|
|
||||||
after(() => {
|
|
||||||
void this.initialize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
this.sourceControlStoreUnsubscribe();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onFiltersUpdated(filters: Filters) {
|
|
||||||
this.filters = filters;
|
|
||||||
},
|
|
||||||
addWorkflow() {
|
|
||||||
this.uiStore.nodeViewInitialized = false;
|
|
||||||
void this.$router.push({
|
|
||||||
name: VIEWS.NEW_WORKFLOW,
|
|
||||||
query: { projectId: this.$route?.params?.projectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$telemetry.track('User clicked add workflow button', {
|
|
||||||
source: 'Workflows list',
|
|
||||||
});
|
|
||||||
this.trackEmptyCardClick('blank');
|
|
||||||
},
|
|
||||||
getTemplateRepositoryURL() {
|
|
||||||
return this.templatesStore.websiteTemplateRepositoryURL;
|
|
||||||
},
|
|
||||||
trackEmptyCardClick(option: 'blank' | 'templates' | 'courses') {
|
|
||||||
this.$telemetry.track('User clicked empty page option', {
|
|
||||||
option,
|
|
||||||
});
|
|
||||||
if (option === 'templates' && this.isSalesUser) {
|
|
||||||
this.trackCategoryLinkClick('Sales');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trackCategoryLinkClick(category: string) {
|
|
||||||
this.$telemetry.track(`User clicked Browse ${category} Templates`, {
|
|
||||||
role: this.usersStore.currentUserCloudInfo?.role,
|
|
||||||
active_workflow_count: this.workflowsStore.activeWorkflows.length,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async initialize() {
|
|
||||||
this.loading = true;
|
|
||||||
await Promise.all([
|
|
||||||
this.usersStore.fetchUsers(),
|
|
||||||
this.workflowsStore.fetchAllWorkflows(this.$route?.params?.projectId as string | undefined),
|
|
||||||
this.workflowsStore.fetchActiveWorkflows(),
|
|
||||||
]);
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
onClickTag(tagId: string) {
|
|
||||||
if (!this.filters.tags.includes(tagId)) {
|
|
||||||
this.filters.tags.push(tagId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFilter(
|
|
||||||
resource: IWorkflowDb,
|
|
||||||
filters: { tags: string[]; search: string; status: string | boolean },
|
|
||||||
matches: boolean,
|
|
||||||
): boolean {
|
|
||||||
if (this.settingsStore.areTagsEnabled && filters.tags.length > 0) {
|
|
||||||
matches =
|
|
||||||
matches &&
|
|
||||||
filters.tags.every((tag) =>
|
|
||||||
(resource.tags as ITag[])?.find((resourceTag) =>
|
|
||||||
typeof resourceTag === 'object'
|
|
||||||
? `${resourceTag.id}` === `${tag}`
|
|
||||||
: `${resourceTag}` === `${tag}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.status !== '') {
|
|
||||||
matches = matches && resource.active === filters.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
},
|
|
||||||
saveFiltersOnQueryString() {
|
|
||||||
const query: { [key: string]: string } = {};
|
|
||||||
|
|
||||||
if (this.filters.search) {
|
|
||||||
query.search = this.filters.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.filters.status !== 'string') {
|
|
||||||
query.status = this.filters.status.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.filters.tags.length) {
|
|
||||||
query.tags = this.filters.tags.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.filters.homeProject) {
|
|
||||||
query.homeProject = this.filters.homeProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.$router.replace({
|
|
||||||
query: Object.keys(query).length ? query : undefined,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isValidProjectId(projectId: string) {
|
|
||||||
return this.projectsStore.availableProjects.some((project) => project.id === projectId);
|
|
||||||
},
|
|
||||||
async removeInvalidQueryFiltersFromUrl(filtersToApply: QueryFilters) {
|
|
||||||
await this.$router.push({
|
|
||||||
query: {
|
|
||||||
...(filtersToApply.tags && { tags: filtersToApply.tags?.join(',') }),
|
|
||||||
...(filtersToApply.status && { status: filtersToApply.status?.toString() }),
|
|
||||||
...(filtersToApply.search && { search: filtersToApply.search }),
|
|
||||||
...(filtersToApply.homeProject && { homeProject: filtersToApply.homeProject }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async setFiltersFromQueryString() {
|
|
||||||
const { tags, status, search, homeProject } = this.$route.query;
|
|
||||||
|
|
||||||
const filtersToApply: QueryFilters = {};
|
|
||||||
|
|
||||||
if (homeProject && typeof homeProject === 'string') {
|
|
||||||
await this.projectsStore.getAvailableProjects();
|
|
||||||
if (this.isValidProjectId(homeProject)) {
|
|
||||||
filtersToApply.homeProject = homeProject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search && typeof search === 'string') {
|
|
||||||
filtersToApply.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags && typeof tags === 'string') {
|
|
||||||
const currentTags = this.tagsStore.allTags.map((tag) => tag.id);
|
|
||||||
const savedTags = tags.split(',').filter((tag) => currentTags.includes(tag));
|
|
||||||
if (savedTags.length) {
|
|
||||||
filtersToApply.tags = savedTags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
status &&
|
|
||||||
typeof status === 'string' &&
|
|
||||||
[StatusFilter.ACTIVE.toString(), StatusFilter.DEACTIVATED.toString()].includes(status)
|
|
||||||
) {
|
|
||||||
filtersToApply.status = status === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.removeInvalidQueryFiltersFromUrl(filtersToApply);
|
|
||||||
|
|
||||||
if (Object.keys(filtersToApply).length) {
|
|
||||||
this.filters = {
|
|
||||||
...this.filters,
|
|
||||||
...filtersToApply,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default WorkflowsView;
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
|
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||||
|
const allWorkflows = computed(() => workflowsStore.allWorkflows as IResource[]);
|
||||||
|
const isShareable = computed(
|
||||||
|
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusFilterOptions = computed(() => [
|
||||||
|
{
|
||||||
|
label: i18n.baseText('workflows.filters.status.all'),
|
||||||
|
value: StatusFilter.ALL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.baseText('workflows.filters.status.active'),
|
||||||
|
value: StatusFilter.ACTIVE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.baseText('workflows.filters.status.deactivated'),
|
||||||
|
value: StatusFilter.DEACTIVATED,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userRole = computed(() => {
|
||||||
|
const role = usersStore.currentUserCloudInfo?.role;
|
||||||
|
if (role) return role;
|
||||||
|
|
||||||
|
const answers = usersStore.currentUser?.personalizationAnswers;
|
||||||
|
if (answers && 'role' in answers) {
|
||||||
|
return answers.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnboardingExperimentEnabled = computed(() => {
|
||||||
|
return (
|
||||||
|
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
||||||
|
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSalesUser = computed(() => {
|
||||||
|
return ['Sales', 'sales-and-marketing'].includes(userRole.value || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
const addWorkflowButtonText = computed(() => {
|
||||||
|
return projectsStore.currentProject
|
||||||
|
? i18n.baseText('workflows.project.add')
|
||||||
|
: i18n.baseText('workflows.add');
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectPermissions = computed(() => {
|
||||||
|
return getResourcePermissions(
|
||||||
|
projectsStore.currentProject?.scopes ?? projectsStore.personalProject?.scopes,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyListDescription = computed(() => {
|
||||||
|
if (readOnlyEnv.value) {
|
||||||
|
return i18n.baseText('workflows.empty.description.readOnlyEnv');
|
||||||
|
} else if (!projectPermissions.value.workflow.create) {
|
||||||
|
return i18n.baseText('workflows.empty.description.noPermission');
|
||||||
|
} else {
|
||||||
|
return i18n.baseText('workflows.empty.description');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFilter = (
|
||||||
|
resource: IWorkflowDb,
|
||||||
|
newFilters: { tags: string[]; search: string; status: string | boolean },
|
||||||
|
matches: boolean,
|
||||||
|
): boolean => {
|
||||||
|
if (settingsStore.areTagsEnabled && newFilters.tags.length > 0) {
|
||||||
|
matches =
|
||||||
|
matches &&
|
||||||
|
newFilters.tags.every((tag) =>
|
||||||
|
(resource.tags as ITag[])?.find((resourceTag) =>
|
||||||
|
typeof resourceTag === 'object'
|
||||||
|
? `${resourceTag.id}` === `${tag}`
|
||||||
|
: `${resourceTag}` === `${tag}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFilters.status !== '') {
|
||||||
|
matches = matches && resource.active === newFilters.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const onFiltersUpdated = (newFilters: Filters) => {
|
||||||
|
Object.assign(filters.value, newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWorkflow = () => {
|
||||||
|
uiStore.nodeViewInitialized = false;
|
||||||
|
void router.push({
|
||||||
|
name: VIEWS.NEW_WORKFLOW,
|
||||||
|
query: { projectId: route.params?.projectId },
|
||||||
|
});
|
||||||
|
|
||||||
|
telemetry.track('User clicked add workflow button', {
|
||||||
|
source: 'Workflows list',
|
||||||
|
});
|
||||||
|
trackEmptyCardClick('blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplateRepositoryURL = () => templatesStore.websiteTemplateRepositoryURL;
|
||||||
|
|
||||||
|
const trackEmptyCardClick = (option: 'blank' | 'templates' | 'courses') => {
|
||||||
|
telemetry.track('User clicked empty page option', {
|
||||||
|
option,
|
||||||
|
});
|
||||||
|
if (option === 'templates' && isSalesUser.value) {
|
||||||
|
trackCategoryLinkClick('Sales');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackCategoryLinkClick = (category: string) => {
|
||||||
|
telemetry.track(`User clicked Browse ${category} Templates`, {
|
||||||
|
role: usersStore.currentUserCloudInfo?.role,
|
||||||
|
active_workflow_count: workflowsStore.activeWorkflows.length,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await Promise.all([
|
||||||
|
usersStore.fetchUsers(),
|
||||||
|
workflowsStore.fetchAllWorkflows(route.params?.projectId as string | undefined),
|
||||||
|
workflowsStore.fetchActiveWorkflows(),
|
||||||
|
]);
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickTag = (tagId: string) => {
|
||||||
|
if (!filters.value.tags.includes(tagId)) {
|
||||||
|
filters.value.tags.push(tagId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFiltersOnQueryString = () => {
|
||||||
|
const query: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
if (filters.value.search) {
|
||||||
|
query.search = filters.value.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof filters.value.status !== 'string') {
|
||||||
|
query.status = filters.value.status.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.tags.length) {
|
||||||
|
query.tags = filters.value.tags.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.homeProject) {
|
||||||
|
query.homeProject = filters.value.homeProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
void router.replace({
|
||||||
|
query: Object.keys(query).length ? query : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function isValidProjectId(projectId: string) {
|
||||||
|
return projectsStore.availableProjects.some((project) => project.id === projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFiltersFromQueryString = async () => {
|
||||||
|
const { tags, status, search, homeProject } = route.query ?? {};
|
||||||
|
|
||||||
|
const filtersToApply: { [key: string]: string | string[] | boolean } = {};
|
||||||
|
|
||||||
|
if (homeProject && typeof homeProject === 'string') {
|
||||||
|
await projectsStore.getAvailableProjects();
|
||||||
|
if (isValidProjectId(homeProject)) {
|
||||||
|
filtersToApply.homeProject = homeProject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search && typeof search === 'string') {
|
||||||
|
filtersToApply.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags && typeof tags === 'string') {
|
||||||
|
await tagsStore.fetchAll();
|
||||||
|
const currentTags = tagsStore.allTags.map((tag) => tag.id);
|
||||||
|
|
||||||
|
filtersToApply.tags = tags.split(',').filter((tag) => currentTags.includes(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
status &&
|
||||||
|
typeof status === 'string' &&
|
||||||
|
[StatusFilter.ACTIVE.toString(), StatusFilter.DEACTIVATED.toString()].includes(status)
|
||||||
|
) {
|
||||||
|
filtersToApply.status = status === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(filtersToApply).length) {
|
||||||
|
Object.assign(filters.value, filtersToApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void router.replace({ query: pickBy(route.query) });
|
||||||
|
};
|
||||||
|
|
||||||
|
sourceControlStore.$onAction(({ name, after }) => {
|
||||||
|
if (name !== 'pullWorkfolder') return;
|
||||||
|
after(async () => await initialize());
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(filters, () => saveFiltersOnQueryString(), { deep: true });
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params?.projectId,
|
||||||
|
async () => await initialize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
documentTitle.set(i18n.baseText('workflows.heading'));
|
||||||
|
await setFiltersFromQueryString();
|
||||||
|
void usersStore.showPersonalizationSurvey();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ResourcesListLayout
|
<ResourcesListLayout
|
||||||
ref="layout"
|
|
||||||
resource-key="workflows"
|
resource-key="workflows"
|
||||||
:resources="allWorkflows"
|
:resources="allWorkflows"
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
|
@ -344,9 +316,9 @@ export default WorkflowsView;
|
||||||
<ProjectTabs />
|
<ProjectTabs />
|
||||||
</template>
|
</template>
|
||||||
<template #add-button="{ disabled }">
|
<template #add-button="{ disabled }">
|
||||||
<n8n-tooltip :disabled="!readOnlyEnv">
|
<N8nTooltip :disabled="!readOnlyEnv">
|
||||||
<div>
|
<div>
|
||||||
<n8n-button
|
<N8nButton
|
||||||
size="large"
|
size="large"
|
||||||
block
|
block
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
@ -354,18 +326,18 @@ export default WorkflowsView;
|
||||||
@click="addWorkflow"
|
@click="addWorkflow"
|
||||||
>
|
>
|
||||||
{{ addWorkflowButtonText }}
|
{{ addWorkflowButtonText }}
|
||||||
</n8n-button>
|
</N8nButton>
|
||||||
</div>
|
</div>
|
||||||
<template #content>
|
<template #content>
|
||||||
<i18n-t tag="span" keypath="mainSidebar.workflows.readOnlyEnv.tooltip">
|
<i18n-t tag="span" keypath="mainSidebar.workflows.readOnlyEnv.tooltip">
|
||||||
<template #link>
|
<template #link>
|
||||||
<a target="_blank" href="https://docs.n8n.io/source-control-environments/">
|
<a target="_blank" href="https://docs.n8n.io/source-control-environments/">
|
||||||
{{ $locale.baseText('mainSidebar.workflows.readOnlyEnv.tooltip.link') }}
|
{{ i18n.baseText('mainSidebar.workflows.readOnlyEnv.tooltip.link') }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</template>
|
</template>
|
||||||
</n8n-tooltip>
|
</N8nTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ data, updateItemSize }">
|
<template #default="{ data, updateItemSize }">
|
||||||
<WorkflowCard
|
<WorkflowCard
|
||||||
|
@ -379,50 +351,50 @@ export default WorkflowsView;
|
||||||
</template>
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="text-center mt-s">
|
<div class="text-center mt-s">
|
||||||
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
|
<N8nHeading tag="h2" size="xlarge" class="mb-2xs">
|
||||||
{{
|
{{
|
||||||
currentUser.firstName
|
currentUser.firstName
|
||||||
? $locale.baseText('workflows.empty.heading', {
|
? i18n.baseText('workflows.empty.heading', {
|
||||||
interpolate: { name: currentUser.firstName },
|
interpolate: { name: currentUser.firstName },
|
||||||
})
|
})
|
||||||
: $locale.baseText('workflows.empty.heading.userNotSetup')
|
: i18n.baseText('workflows.empty.heading.userNotSetup')
|
||||||
}}
|
}}
|
||||||
</n8n-heading>
|
</N8nHeading>
|
||||||
<n8n-text v-if="!isOnboardingExperimentEnabled" size="large" color="text-base">
|
<N8nText v-if="!isOnboardingExperimentEnabled" size="large" color="text-base">
|
||||||
{{ emptyListDescription }}
|
{{ emptyListDescription }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!readOnlyEnv && projectPermissions.workflow.create"
|
v-if="!readOnlyEnv && projectPermissions.workflow.create"
|
||||||
:class="['text-center', 'mt-2xl', $style.actionsContainer]"
|
:class="['text-center', 'mt-2xl', $style.actionsContainer]"
|
||||||
>
|
>
|
||||||
<n8n-card
|
<N8nCard
|
||||||
:class="$style.emptyStateCard"
|
:class="$style.emptyStateCard"
|
||||||
hoverable
|
hoverable
|
||||||
data-test-id="new-workflow-card"
|
data-test-id="new-workflow-card"
|
||||||
@click="addWorkflow"
|
@click="addWorkflow"
|
||||||
>
|
>
|
||||||
<n8n-icon :class="$style.emptyStateCardIcon" icon="file" />
|
<N8nIcon :class="$style.emptyStateCardIcon" icon="file" />
|
||||||
<n8n-text size="large" class="mt-xs" color="text-dark">
|
<N8nText size="large" class="mt-xs" color="text-dark">
|
||||||
{{ $locale.baseText('workflows.empty.startFromScratch') }}
|
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</n8n-card>
|
</N8nCard>
|
||||||
<a
|
<a
|
||||||
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
||||||
href="https://docs.n8n.io/courses/#available-courses"
|
href="https://docs.n8n.io/courses/#available-courses"
|
||||||
:class="$style.emptyStateCard"
|
:class="$style.emptyStateCard"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<n8n-card
|
<N8nCard
|
||||||
hoverable
|
hoverable
|
||||||
data-test-id="browse-sales-templates-card"
|
data-test-id="browse-sales-templates-card"
|
||||||
@click="trackEmptyCardClick('courses')"
|
@click="trackEmptyCardClick('courses')"
|
||||||
>
|
>
|
||||||
<n8n-icon :class="$style.emptyStateCardIcon" icon="graduation-cap" />
|
<N8nIcon :class="$style.emptyStateCardIcon" icon="graduation-cap" />
|
||||||
<n8n-text size="large" class="mt-xs" color="text-dark">
|
<N8nText size="large" class="mt-xs" color="text-dark">
|
||||||
{{ $locale.baseText('workflows.empty.learnN8n') }}
|
{{ i18n.baseText('workflows.empty.learnN8n') }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</n8n-card>
|
</N8nCard>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
||||||
|
@ -430,57 +402,57 @@ export default WorkflowsView;
|
||||||
:class="$style.emptyStateCard"
|
:class="$style.emptyStateCard"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<n8n-card
|
<N8nCard
|
||||||
hoverable
|
hoverable
|
||||||
data-test-id="browse-sales-templates-card"
|
data-test-id="browse-sales-templates-card"
|
||||||
@click="trackEmptyCardClick('templates')"
|
@click="trackEmptyCardClick('templates')"
|
||||||
>
|
>
|
||||||
<n8n-icon :class="$style.emptyStateCardIcon" icon="box-open" />
|
<N8nIcon :class="$style.emptyStateCardIcon" icon="box-open" />
|
||||||
<n8n-text size="large" class="mt-xs" color="text-dark">
|
<N8nText size="large" class="mt-xs" color="text-dark">
|
||||||
{{ $locale.baseText('workflows.empty.browseTemplates') }}
|
{{ i18n.baseText('workflows.empty.browseTemplates') }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</n8n-card>
|
</N8nCard>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #filters="{ setKeyValue }">
|
<template #filters="{ setKeyValue }">
|
||||||
<div v-if="settingsStore.areTagsEnabled" class="mb-s">
|
<div v-if="settingsStore.areTagsEnabled" class="mb-s">
|
||||||
<n8n-input-label
|
<N8nInputLabel
|
||||||
:label="$locale.baseText('workflows.filters.tags')"
|
:label="i18n.baseText('workflows.filters.tags')"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
size="small"
|
size="small"
|
||||||
color="text-base"
|
color="text-base"
|
||||||
class="mb-3xs"
|
class="mb-3xs"
|
||||||
/>
|
/>
|
||||||
<WorkflowTagsDropdown
|
<WorkflowTagsDropdown
|
||||||
:placeholder="$locale.baseText('workflowOpen.filterWorkflows')"
|
:placeholder="i18n.baseText('workflowOpen.filterWorkflows')"
|
||||||
:model-value="filters.tags"
|
:model-value="filters.tags"
|
||||||
:create-enabled="false"
|
:create-enabled="false"
|
||||||
@update:model-value="setKeyValue('tags', $event)"
|
@update:model-value="setKeyValue('tags', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-s">
|
<div class="mb-s">
|
||||||
<n8n-input-label
|
<N8nInputLabel
|
||||||
:label="$locale.baseText('workflows.filters.status')"
|
:label="i18n.baseText('workflows.filters.status')"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
size="small"
|
size="small"
|
||||||
color="text-base"
|
color="text-base"
|
||||||
class="mb-3xs"
|
class="mb-3xs"
|
||||||
/>
|
/>
|
||||||
<n8n-select
|
<N8nSelect
|
||||||
data-test-id="status-dropdown"
|
data-test-id="status-dropdown"
|
||||||
:model-value="filters.status"
|
:model-value="filters.status"
|
||||||
@update:model-value="setKeyValue('status', $event)"
|
@update:model-value="setKeyValue('status', $event)"
|
||||||
>
|
>
|
||||||
<n8n-option
|
<N8nOption
|
||||||
v-for="option in statusFilterOptions"
|
v-for="option in statusFilterOptions"
|
||||||
:key="option.label"
|
:key="option.label"
|
||||||
:label="option.label"
|
:label="option.label"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
data-test-id="status"
|
data-test-id="status"
|
||||||
>
|
>
|
||||||
</n8n-option>
|
</N8nOption>
|
||||||
</n8n-select>
|
</N8nSelect>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ResourcesListLayout>
|
</ResourcesListLayout>
|
||||||
|
|
Loading…
Reference in a new issue