refactor(editor): Convert Sticky component to Options API (no-changelog) (#10975)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-09-26 14:15:11 +02:00 committed by GitHub
parent 06d749ffa7
commit b7951a071c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 280 additions and 315 deletions

View file

@ -136,7 +136,7 @@ const htmlContent = computed(() => {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
'markdown-click': [link: string, e: MouseEvent]; 'markdown-click': [link: HTMLAnchorElement, e: MouseEvent];
'update-content': [content: string]; 'update-content': [content: string];
}>(); }>();
@ -154,7 +154,7 @@ const onClick = (event: MouseEvent) => {
} }
} }
if (clickedLink) { if (clickedLink) {
emit('markdown-click', clickedLink?.href, event); emit('markdown-click', clickedLink, event);
} }
}; };

View file

@ -21,6 +21,7 @@ const emit = defineEmits<{
resize: [values: ResizeData]; resize: [values: ResizeData];
resizestart: []; resizestart: [];
resizeend: []; resizeend: [];
'markdown-click': [link: HTMLAnchorElement, e: MouseEvent];
}>(); }>();
const attrs = useAttrs(); const attrs = useAttrs();
@ -42,6 +43,10 @@ const onResizeEnd = () => {
isResizing.value = false; isResizing.value = false;
emit('resizeend'); emit('resizeend');
}; };
const onMarkdownClick = (link: HTMLAnchorElement, event: MouseEvent) => {
emit('markdown-click', link, event);
};
</script> </script>
<template> <template>
@ -57,6 +62,6 @@ const onResizeEnd = () => {
@resize="onResize" @resize="onResize"
@resizestart="onResizeStart" @resizestart="onResizeStart"
> >
<N8nSticky v-bind="stickyBindings" /> <N8nSticky v-bind="stickyBindings" @markdown-click="onMarkdownClick" />
</N8nResizeWrapper> </N8nResizeWrapper>
</template> </template>

View file

@ -13,7 +13,7 @@ const props = withDefaults(defineProps<StickyProps>(), defaultStickyProps);
const emit = defineEmits<{ const emit = defineEmits<{
edit: [editing: boolean]; edit: [editing: boolean];
'update:modelValue': [value: string]; 'update:modelValue': [value: string];
'markdown-click': [link: string, e: Event]; 'markdown-click': [link: HTMLAnchorElement, e: MouseEvent];
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -63,7 +63,7 @@ const onUpdateModelValue = (value: string) => {
emit('update:modelValue', value); emit('update:modelValue', value);
}; };
const onMarkdownClick = (link: string, event: Event) => { const onMarkdownClick = (link: HTMLAnchorElement, event: MouseEvent) => {
emit('markdown-click', link, event); emit('markdown-click', link, event);
}; };

View file

@ -1,19 +1,12 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref } from 'vue'; import { ref, computed, onMounted, nextTick } from 'vue';
import type { PropType, StyleValue } from 'vue'; import type { StyleValue } from 'vue';
import { mapStores } from 'pinia';
import { onClickOutside } from '@vueuse/core'; import { onClickOutside } from '@vueuse/core';
import type { Workflow } from 'n8n-workflow';
import { isNumber, isString } from '@/utils/typeGuards'; import { isNumber, isString } from '@/utils/typeGuards';
import type { import type { INodeUi, XYPosition } from '@/Interface';
INodeUi,
INodeUpdatePropertiesInformation,
IUpdateInformation,
XYPosition,
} from '@/Interface';
import type { INodeTypeDescription, Workflow } from 'n8n-workflow';
import { QUICKSTART_NOTE_NAME } from '@/constants'; import { QUICKSTART_NOTE_NAME } from '@/constants';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
@ -25,61 +18,53 @@ import { GRID_SIZE } from '@/utils/nodeViewUtils';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { assert } from '@/utils/assert'; import { assert } from '@/utils/assert';
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
import { useCanvasStore } from '@/stores/canvas.store';
import { useHistoryStore } from '@/stores/history.store';
import { useNodeBase } from '@/composables/useNodeBase'; import { useNodeBase } from '@/composables/useNodeBase';
import { useTelemetry } from '@/composables/useTelemetry';
export default defineComponent({ const props = withDefaults(
name: 'Sticky', defineProps<{
props: { nodeViewScale?: number;
nodeViewScale: { gridSize?: number;
type: Number, name: string;
default: 1, instance: BrowserJsPlumbInstance;
isReadOnly?: boolean;
isActive?: boolean;
hideActions?: boolean;
disableSelecting?: boolean;
showCustomTooltip?: boolean;
workflow: Workflow;
}>(),
{
nodeViewScale: 1,
gridSize: GRID_SIZE,
}, },
gridSize: { );
type: Number,
default: GRID_SIZE, defineOptions({ name: 'Sticky' });
},
name: { const emit = defineEmits<{
type: String, removeNode: [string];
required: true, nodeSelected: [string, boolean, boolean];
}, }>();
instance: {
type: Object as PropType<BrowserJsPlumbInstance>,
required: true,
},
isReadOnly: {
type: Boolean,
},
isActive: {
type: Boolean,
},
hideActions: {
type: Boolean,
},
disableSelecting: {
type: Boolean,
},
showCustomTooltip: {
type: Boolean,
},
workflow: {
type: Object as PropType<Workflow>,
required: true,
},
},
emits: { removeNode: null, nodeSelected: null },
setup(props, { emit }) {
const deviceSupport = useDeviceSupport(); const deviceSupport = useDeviceSupport();
const telemetry = useTelemetry();
const toast = useToast(); const toast = useToast();
const ndvStore = useNDVStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const isResizing = ref<boolean>(false);
const isTouchActive = ref<boolean>(false);
const forceActions = ref(false); const forceActions = ref(false);
const isColorPopoverVisible = ref(false); const isColorPopoverVisible = ref(false);
const stickOptions = ref<HTMLElement>(); const stickOptions = ref<HTMLElement>();
const setForceActions = (value: boolean) => { const setForceActions = (value: boolean) => {
forceActions.value = value; forceActions.value = value;
}; };
const setColorPopoverVisible = (value: boolean) => { const setColorPopoverVisible = (value: boolean) => {
isColorPopoverVisible.value = value; isColorPopoverVisible.value = value;
}; };
@ -101,7 +86,7 @@ export default defineComponent({
onClickOutside(stickOptions, () => setColorPopoverVisible(false)); onClickOutside(stickOptions, () => setColorPopoverVisible(false));
return { defineExpose({
deviceSupport, deviceSupport,
toast, toast,
contextMenu, contextMenu,
@ -111,141 +96,103 @@ export default defineComponent({
isColorPopoverVisible, isColorPopoverVisible,
setColorPopoverVisible, setColorPopoverVisible,
stickOptions, stickOptions,
}; });
},
data() { const data = computed(() => workflowsStore.getNodeByName(props.name));
return { // TODO: remove either node or data
isResizing: false, const node = computed(() => workflowsStore.getNodeByName(props.name));
isTouchActive: false, const nodeId = computed(() => data.value?.id);
}; const nodeType = computed(() => {
}, return data.value && nodeTypesStore.getNodeType(data.value.type, data.value.typeVersion);
computed: { });
...mapStores( const defaultText = computed(() => {
useNodeTypesStore, if (!nodeType.value) {
useUIStore,
useNDVStore,
useCanvasStore,
useWorkflowsStore,
useHistoryStore,
),
data(): INodeUi | null {
return this.workflowsStore.getNodeByName(this.name);
},
nodeId(): string {
return this.data?.id || '';
},
defaultText(): string {
if (!this.nodeType) {
return ''; return '';
} }
const properties = this.nodeType.properties; const properties = nodeType.value.properties;
const content = properties.find((property) => property.name === 'content'); const content = properties.find((property) => property.name === 'content');
return content && isString(content.default) ? content.default : ''; return content && isString(content.default) ? content.default : '';
}, });
isSelected(): boolean { const isSelected = computed(
return ( () =>
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data?.name) !== uiStore.getSelectedNodes.find(({ name }: INodeUi) => name === data.value?.name) !== undefined,
undefined
); );
},
nodeType(): INodeTypeDescription | null {
return this.data && this.nodeTypesStore.getNodeType(this.data.type, this.data.typeVersion);
},
node(): INodeUi | null {
// same as this.data but reactive..
return this.workflowsStore.getNodeByName(this.name);
},
position(): XYPosition {
if (this.node) {
return this.node.position;
} else {
return [0, 0];
}
},
height(): number {
return this.node && isNumber(this.node.parameters.height) ? this.node.parameters.height : 0;
},
width(): number {
return this.node && isNumber(this.node.parameters.width) ? this.node.parameters.width : 0;
},
stickySize(): StyleValue {
const returnStyles: {
[key: string]: string | number;
} = {
height: this.height + 'px',
width: this.width + 'px',
};
return returnStyles; const position = computed<XYPosition>(() => (node.value ? node.value.position : [0, 0]));
},
stickyPosition(): StyleValue {
const returnStyles: {
[key: string]: string | number;
} = {
left: this.position[0] + 'px',
top: this.position[1] + 'px',
zIndex: this.isActive ? 9999999 : -1 * Math.floor((this.height * this.width) / 1000),
};
return returnStyles; const height = computed(() =>
}, node.value && isNumber(node.value.parameters.height) ? node.value.parameters.height : 0,
showActions(): boolean {
return (
!(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing) ||
this.forceActions
); );
},
workflowRunning(): boolean { const width = computed(() =>
return this.uiStore.isActionActive['workflowRunning']; node.value && isNumber(node.value.parameters.width) ? node.value.parameters.width : 0,
}, );
},
mounted() { const stickySize = computed<StyleValue>(() => ({
height: height.value + 'px',
width: width.value + 'px',
}));
const stickyPosition = computed<StyleValue>(() => ({
left: position.value[0] + 'px',
top: position.value[1] + 'px',
zIndex: props.isActive ? 9999999 : -1 * Math.floor((height.value * width.value) / 1000),
}));
const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
const showActions = computed(
() =>
!(props.hideActions || props.isReadOnly || workflowRunning.value || isResizing.value) ||
forceActions.value,
);
onMounted(() => {
// Initialize the node // Initialize the node
if (this.data !== null) { if (data.value !== null) {
try { try {
this.addNode(this.data); nodeBase.addNode(data.value);
} catch (error) { } catch (error) {
// This breaks when new nodes are loaded into store but workflow tab is not currently active // This breaks when new nodes are loaded into store but workflow tab is not currently active
// Shouldn't affect anything // Shouldn't affect anything
} }
} }
}, });
methods: {
onShowPopover() { const onShowPopover = () => setForceActions(true);
this.setForceActions(true); const onHidePopover = () => setForceActions(false);
}, const deleteNode = async () => {
onHidePopover() { assert(data.value);
this.setForceActions(false);
},
async deleteNode() {
assert(this.data);
// Wait a tick else vue causes problems because the data is gone // Wait a tick else vue causes problems because the data is gone
await this.$nextTick(); await nextTick();
this.$emit('removeNode', this.data.name);
}, emit('removeNode', data.value.name);
changeColor(index: number) { };
this.workflowsStore.updateNodeProperties({
name: this.name, const changeColor = (index: number) => {
workflowsStore.updateNodeProperties({
name: props.name,
properties: { properties: {
parameters: { parameters: {
...this.node?.parameters, ...node.value?.parameters,
color: index, color: index,
}, },
position: this.node?.position ?? [0, 0], position: node.value?.position ?? [0, 0],
}, },
}); });
}, };
onEdit(edit: boolean) {
if (edit && !this.isActive && this.node) { const onEdit = (edit: boolean) => {
this.ndvStore.activeNodeName = this.node.name; if (edit && !props.isActive && node.value) {
} else if (this.isActive && !edit) { ndvStore.activeNodeName = node.value.name;
this.ndvStore.activeNodeName = null; } else if (props.isActive && !edit) {
ndvStore.activeNodeName = null;
} }
}, };
onMarkdownClick(link: HTMLAnchorElement) {
const onMarkdownClick = (link: HTMLAnchorElement) => {
if (link) { if (link) {
const isOnboardingNote = this.name === QUICKSTART_NOTE_NAME; const isOnboardingNote = props.name === QUICKSTART_NOTE_NAME;
const isWelcomeVideo = link.querySelector('img[alt="n8n quickstart video"]'); const isWelcomeVideo = link.querySelector('img[alt="n8n quickstart video"]');
const type = const type =
isOnboardingNote && isWelcomeVideo isOnboardingNote && isWelcomeVideo
@ -254,84 +201,97 @@ export default defineComponent({
? 'templates' ? 'templates'
: 'other'; : 'other';
this.$telemetry.track('User clicked note link', { type }); telemetry.track('User clicked note link', { type });
} }
}, };
onInputChange(content: string) {
if (!this.node) { const setParameters = (params: {
content?: string;
height?: number;
width?: number;
color?: string;
}) => {
if (node.value) {
const nodeParameters = {
content: isString(params.content) ? params.content : node.value.parameters.content,
height: isNumber(params.height) ? params.height : node.value.parameters.height,
width: isNumber(params.width) ? params.width : node.value.parameters.width,
color: isString(params.color) ? params.color : node.value.parameters.color,
};
workflowsStore.setNodeParameters({
key: node.value.id,
name: node.value.name,
value: nodeParameters,
});
}
};
const onInputChange = (content: string) => {
if (!node.value) {
return; return;
} }
this.node.parameters.content = content; node.value.parameters.content = content;
this.setParameters({ content }); setParameters({ content });
}, };
onResizeStart() {
this.isResizing = true; const setPosition = (newPosition: XYPosition) => {
if (!this.isSelected && this.node) { if (!node.value) return;
this.$emit('nodeSelected', this.node.name, false, true);
workflowsStore.updateNodeProperties({
name: node.value.name,
properties: { position: newPosition },
});
};
const onResizeStart = () => {
isResizing.value = true;
if (!isSelected.value && node.value) {
emit('nodeSelected', node.value.name, false, true);
} }
}, };
onResize({ height, width, dX, dY }: { width: number; height: number; dX: number; dY: number }) {
if (!this.node) { const onResize = ({
height,
width,
dX,
dY,
}: {
width: number;
height: number;
dX: number;
dY: number;
}) => {
if (!node.value) {
return; return;
} }
if (dX !== 0 || dY !== 0) { if (dX !== 0 || dY !== 0) {
this.setPosition([this.node.position[0] + (dX || 0), this.node.position[1] + (dY || 0)]); setPosition([node.value.position[0] + (dX || 0), node.value.position[1] + (dY || 0)]);
} }
this.setParameters({ height, width }); setParameters({ height, width });
},
onResizeEnd() {
this.isResizing = false;
},
setParameters(params: { content?: string; height?: number; width?: number; color?: string }) {
if (this.node) {
const nodeParameters = {
content: isString(params.content) ? params.content : this.node.parameters.content,
height: isNumber(params.height) ? params.height : this.node.parameters.height,
width: isNumber(params.width) ? params.width : this.node.parameters.width,
color: isString(params.color) ? params.color : this.node.parameters.color,
}; };
const updateInformation: IUpdateInformation = { const onResizeEnd = () => {
key: this.node.id, isResizing.value = false;
name: this.node.name,
value: nodeParameters,
}; };
this.workflowsStore.setNodeParameters(updateInformation); const touchStart = () => {
} if (deviceSupport.isTouchDevice && !deviceSupport.isMacOs && !isTouchActive.value) {
}, isTouchActive.value = true;
setPosition(position: XYPosition) {
if (!this.node) {
return;
}
const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name,
properties: {
position,
},
};
this.workflowsStore.updateNodeProperties(updateInformation);
},
touchStart() {
if (this.deviceSupport.isTouchDevice && !this.deviceSupport.isMacOs && !this.isTouchActive) {
this.isTouchActive = true;
setTimeout(() => { setTimeout(() => {
this.isTouchActive = false; isTouchActive.value = false;
}, 2000); }, 2000);
} }
}, };
onContextMenu(e: MouseEvent): void {
if (this.node && !this.isActive) { const onContextMenu = (e: MouseEvent): void => {
this.contextMenu.open(e, { source: 'node-right-click', nodeId: this.node.id }); if (node.value && !props.isActive) {
contextMenu.open(e, { source: 'node-right-click', nodeId: node.value.id });
} else { } else {
e.stopPropagation(); e.stopPropagation();
} }
}, };
},
});
</script> </script>
<template> <template>
@ -355,9 +315,9 @@ export default defineComponent({
<div v-show="isSelected" class="select-sticky-background" /> <div v-show="isSelected" class="select-sticky-background" />
<div <div
v-touch:start="touchStart" v-touch:start="touchStart"
v-touch:end="touchEnd" v-touch:end="nodeBase.touchEnd"
class="sticky-box" class="sticky-box"
@click.left="mouseLeftClick" @click.left="nodeBase.mouseLeftClick"
@contextmenu="onContextMenu" @contextmenu="onContextMenu"
> >
<N8nResizeableSticky <N8nResizeableSticky