mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(editor): Migrate header WorkflowDetails to composition api (no-changelog) (#9186)
This commit is contained in:
parent
442aaba116
commit
1c261f85a3
|
@ -61,23 +61,13 @@ import { ref, useCssModule, useAttrs, computed } from 'vue';
|
||||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
|
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut';
|
import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut';
|
||||||
import type { KeyboardShortcut } from '../../types';
|
import type { ActionDropdownItem } from '../../types';
|
||||||
import type { IconSize } from '@/types/icon';
|
import type { IconSize } from '@/types/icon';
|
||||||
|
|
||||||
interface IActionDropdownItem {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon?: string;
|
|
||||||
divided?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
shortcut?: KeyboardShortcut;
|
|
||||||
customClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRIGGER = ['click', 'hover'] as const;
|
const TRIGGER = ['click', 'hover'] as const;
|
||||||
|
|
||||||
interface ActionDropdownProps {
|
interface ActionDropdownProps {
|
||||||
items: IActionDropdownItem[];
|
items: ActionDropdownItem[];
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
activatorIcon?: string;
|
activatorIcon?: string;
|
||||||
activatorSize?: IconSize;
|
activatorSize?: IconSize;
|
||||||
|
@ -99,7 +89,7 @@ const $attrs = useAttrs();
|
||||||
const testIdPrefix = $attrs['data-test-id'];
|
const testIdPrefix = $attrs['data-test-id'];
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const getItemClasses = (item: IActionDropdownItem): Record<string, boolean> => {
|
const getItemClasses = (item: ActionDropdownItem): Record<string, boolean> => {
|
||||||
return {
|
return {
|
||||||
[$style.itemContainer]: true,
|
[$style.itemContainer]: true,
|
||||||
[$style.disabled]: !!item.disabled,
|
[$style.disabled]: !!item.disabled,
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
border-radius: var.$tag-border-radius;
|
border-radius: var.$tag-border-radius;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
.el-icon.el-tag__close {
|
.el-icon.el-tag__close {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
@ -137,9 +138,8 @@
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
vertical-align: middle;
|
margin-top: 0;
|
||||||
top: -1px;
|
margin-right: 0;
|
||||||
right: -5px;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
11
packages/design-system/src/types/action-dropdown.ts
Normal file
11
packages/design-system/src/types/action-dropdown.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { KeyboardShortcut } from '@/types/keyboardshortcut';
|
||||||
|
|
||||||
|
export interface ActionDropdownItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
divided?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
shortcut?: KeyboardShortcut;
|
||||||
|
customClass?: string;
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './action-dropdown';
|
||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './datatable';
|
export * from './datatable';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
|
|
|
@ -19,3 +19,28 @@ Range.prototype.getClientRects = vi.fn(() => ({
|
||||||
length: 0,
|
length: 0,
|
||||||
[Symbol.iterator]: vi.fn(),
|
[Symbol.iterator]: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export class IntersectionObserver {
|
||||||
|
root = null;
|
||||||
|
rootMargin = '';
|
||||||
|
thresholds = [];
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
observe() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
takeRecords() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
unobserve() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.IntersectionObserver = IntersectionObserver;
|
||||||
|
global.IntersectionObserver = IntersectionObserver;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
|
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
|
||||||
<div v-show="!hideMenuBar" class="top-menu">
|
<div v-show="!hideMenuBar" class="top-menu">
|
||||||
<WorkflowDetails :read-only="readOnly" />
|
<WorkflowDetails v-if="workflow?.name" :workflow="workflow" :read-only="readOnly" />
|
||||||
<TabBar
|
<TabBar
|
||||||
v-if="onWorkflowPage"
|
v-if="onWorkflowPage"
|
||||||
:items="tabBarItems"
|
:items="tabBarItems"
|
||||||
|
@ -27,7 +27,7 @@ import {
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type { INodeUi, ITabBarItem } from '@/Interface';
|
import type { INodeUi, ITabBarItem, IWorkflowDb } from '@/Interface';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
@ -75,6 +75,9 @@ export default defineComponent({
|
||||||
hideMenuBar(): boolean {
|
hideMenuBar(): boolean {
|
||||||
return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
|
return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
|
||||||
},
|
},
|
||||||
|
workflow(): IWorkflowDb {
|
||||||
|
return this.workflowsStore.workflow;
|
||||||
|
},
|
||||||
workflowName(): string {
|
workflowName(): string {
|
||||||
return this.workflowsStore.workflowName;
|
return this.workflowsStore.workflowName;
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { fireEvent } from '@testing-library/vue';
|
||||||
|
|
||||||
|
vi.mock('vue-router', async () => {
|
||||||
|
const actual = await import('vue-router');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useRoute: () => ({
|
||||||
|
value: {
|
||||||
|
params: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
[STORES.SETTINGS]: {
|
||||||
|
settings: {
|
||||||
|
enterprise: {
|
||||||
|
sharing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
areTagsEnabled: true,
|
||||||
|
},
|
||||||
|
[STORES.TAGS]: {
|
||||||
|
tags: {
|
||||||
|
1: {
|
||||||
|
id: '1',
|
||||||
|
name: 'tag1',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: '2',
|
||||||
|
name: 'tag2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(WorkflowDetails, {
|
||||||
|
pinia: createTestingPinia({ initialState }),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WorkflowDetails', () => {
|
||||||
|
it('renders workflow name and tags', async () => {
|
||||||
|
const workflow = {
|
||||||
|
id: '1',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
tags: ['1', '2'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = renderComponent({
|
||||||
|
props: {
|
||||||
|
workflow,
|
||||||
|
readOnly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowName = getByTestId('workflow-name-input');
|
||||||
|
const workflowNameInput = workflowName.querySelector('input');
|
||||||
|
|
||||||
|
expect(workflowNameInput).toHaveValue('Test Workflow');
|
||||||
|
expect(getByText('tag1')).toBeInTheDocument();
|
||||||
|
expect(getByText('tag2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls save function on save button click', async () => {
|
||||||
|
const onSaveButtonClick = vi.fn();
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
workflow: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
onSaveButtonClick,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fireEvent.click(getByTestId('workflow-save-button'));
|
||||||
|
expect(onSaveButtonClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens share modal on share button click', async () => {
|
||||||
|
const onShareButtonClick = vi.fn();
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
workflow: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
onShareButtonClick,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fireEvent.click(getByTestId('workflow-share-button'));
|
||||||
|
expect(onShareButtonClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load diff
|
@ -318,7 +318,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tag {
|
.el-tag {
|
||||||
padding: 1px var(--spacing-4xs);
|
padding: var(--spacing-5xs) var(--spacing-4xs);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
background-color: var(--color-background-base);
|
background-color: var(--color-background-base);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import type { XYPosition } from '@/Interface';
|
import type { ActionDropdownItem, XYPosition } from '@/Interface';
|
||||||
import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
|
import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.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 type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue';
|
|
||||||
import type { INode, INodeTypeDescription } from 'n8n-workflow';
|
import type { INode, INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { getMousePosition } from '../utils/nodeViewUtils';
|
import { getMousePosition } from '../utils/nodeViewUtils';
|
||||||
|
@ -34,7 +33,7 @@ export type ContextMenuAction =
|
||||||
const position = ref<XYPosition>([0, 0]);
|
const position = ref<XYPosition>([0, 0]);
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
const target = ref<ContextMenuTarget>({ source: 'canvas' });
|
const target = ref<ContextMenuTarget>({ source: 'canvas' });
|
||||||
const actions = ref<IActionDropdownItem[]>([]);
|
const actions = ref<ActionDropdownItem[]>([]);
|
||||||
const actionCallback = ref<ContextMenuActionCallback>(() => {});
|
const actionCallback = ref<ContextMenuActionCallback>(() => {});
|
||||||
|
|
||||||
export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) => {
|
export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) => {
|
||||||
|
@ -147,7 +146,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
...selectionActions,
|
...selectionActions,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const menuActions: IActionDropdownItem[] = [
|
const menuActions: ActionDropdownItem[] = [
|
||||||
!onlyStickies && {
|
!onlyStickies && {
|
||||||
id: 'toggle_activation',
|
id: 'toggle_activation',
|
||||||
label: nodes.every((node) => node.disabled)
|
label: nodes.every((node) => node.disabled)
|
||||||
|
@ -183,7 +182,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
|
||||||
shortcut: { keys: ['Del'] },
|
shortcut: { keys: ['Del'] },
|
||||||
disabled: isReadOnly.value,
|
disabled: isReadOnly.value,
|
||||||
},
|
},
|
||||||
].filter(Boolean) as IActionDropdownItem[];
|
].filter(Boolean) as ActionDropdownItem[];
|
||||||
|
|
||||||
if (nodes.length === 1) {
|
if (nodes.length === 1) {
|
||||||
const singleNodeActions = onlyStickies
|
const singleNodeActions = onlyStickies
|
||||||
|
|
|
@ -354,7 +354,7 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
|
|
||||||
&.is-closable {
|
&.is-closable {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -363,7 +363,7 @@
|
||||||
.el-tag__close {
|
.el-tag__close {
|
||||||
max-height: 15px;
|
max-height: 15px;
|
||||||
max-width: 15px;
|
max-width: 15px;
|
||||||
margin-right: 6px;
|
margin-left: var(--spacing-4xs);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $tag-close-background-hover-color !important;
|
background-color: $tag-close-background-hover-color !important;
|
||||||
|
|
Loading…
Reference in a new issue