mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Migrate copyPaste mixin to composables (no-changelog) (#8179)
This commit is contained in:
parent
216ec079c9
commit
f5a4bfe40c
|
@ -50,7 +50,6 @@
|
|||
"chart.js": "^4.4.0",
|
||||
"codemirror-lang-html-n8n": "^1.0.0",
|
||||
"codemirror-lang-n8n-expression": "^0.2.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dateformat": "^3.0.3",
|
||||
"esprima-next": "5.8.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [copyPaste],
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
|
@ -68,14 +67,17 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
copy(): void {
|
||||
this.$emit('copy');
|
||||
this.copyToClipboard(this.value);
|
||||
void this.clipboard.copy(this.value);
|
||||
|
||||
this.showMessage({
|
||||
title: this.toastTitle,
|
||||
|
|
|
@ -121,7 +121,6 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
|
||||
|
||||
|
@ -134,16 +133,19 @@ import type {
|
|||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeErrorView',
|
||||
components: {
|
||||
VueJsonPretty,
|
||||
},
|
||||
mixins: [copyPaste],
|
||||
props: ['error'],
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -283,7 +285,7 @@ export default defineComponent({
|
|||
return [currentParameter];
|
||||
},
|
||||
copyCause() {
|
||||
this.copyToClipboard(JSON.stringify(this.error.cause));
|
||||
void this.clipboard.copy(JSON.stringify(this.error.cause));
|
||||
this.copySuccess();
|
||||
},
|
||||
copySuccess() {
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import Modal from './Modal.vue';
|
||||
import type { IFormInputs, IInviteResponse, IUser } from '@/Interface';
|
||||
import { ROLE } from '@/utils/userUtils';
|
||||
|
@ -73,6 +72,7 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
|
||||
|
||||
|
@ -90,14 +90,16 @@ function getEmail(email: string): string {
|
|||
export default defineComponent({
|
||||
name: 'InviteUsersModal',
|
||||
components: { Modal },
|
||||
mixins: [copyPaste],
|
||||
props: {
|
||||
modalName: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -258,7 +260,7 @@ export default defineComponent({
|
|||
|
||||
if (successfulUrlInvites.length) {
|
||||
if (successfulUrlInvites.length === 1) {
|
||||
this.copyToClipboard(successfulUrlInvites[0].user.inviteAcceptUrl);
|
||||
void this.clipboard.copy(successfulUrlInvites[0].user.inviteAcceptUrl);
|
||||
}
|
||||
|
||||
this.showMessage({
|
||||
|
@ -328,7 +330,7 @@ export default defineComponent({
|
|||
},
|
||||
onCopyInviteLink(user: IUser) {
|
||||
if (user.inviteAcceptUrl && this.showInviteUrls) {
|
||||
this.copyToClipboard(user.inviteAcceptUrl);
|
||||
void this.clipboard.copy(user.inviteAcceptUrl);
|
||||
this.showCopyInviteLinkToast([]);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -140,20 +140,22 @@ import { mapStores } from 'pinia';
|
|||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { mfaEventBus } from '@/event-bus';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
//@ts-ignore
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
export default defineComponent({
|
||||
name: 'MfaSetupModal',
|
||||
components: {
|
||||
Modal,
|
||||
QrcodeVue,
|
||||
},
|
||||
mixins: [copyPaste],
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -199,7 +201,7 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
onCopySecretToClipboard() {
|
||||
this.copyToClipboard(this.secret);
|
||||
void this.clipboard.copy(this.secret);
|
||||
this.showToast({
|
||||
title: this.$locale.baseText('mfa.setup.step1.toast.copyToClipboard.title'),
|
||||
message: this.$locale.baseText('mfa.setup.step1.toast.copyToClipboard.message'),
|
||||
|
|
|
@ -66,18 +66,21 @@ import { defineComponent } from 'vue';
|
|||
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeWebhooks',
|
||||
mixins: [copyPaste, workflowHelpers],
|
||||
mixins: [workflowHelpers],
|
||||
props: [
|
||||
'node', // NodeUi
|
||||
'nodeType', // INodeTypeDescription
|
||||
],
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -136,7 +139,7 @@ export default defineComponent({
|
|||
methods: {
|
||||
copyWebhookUrl(webhookData: IWebhookDescription): void {
|
||||
const webhookUrl = this.getWebhookUrlDisplay(webhookData);
|
||||
this.copyToClipboard(webhookUrl);
|
||||
void this.clipboard.copy(webhookUrl);
|
||||
|
||||
this.showMessage({
|
||||
title: this.baseText.copyTitle,
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
type="secondary"
|
||||
:title="$locale.baseText('nodeErrorView.copyToClipboard')"
|
||||
icon="copy"
|
||||
@click="copyToClipboard(raw)"
|
||||
@click="onCopyToClipboard(raw)"
|
||||
/>
|
||||
<VueMarkdown :source="jsonToMarkdown(raw as JsonMarkdown)" :class="$style.markdown" />
|
||||
</div>
|
||||
|
@ -68,7 +68,7 @@ import { ref, onMounted } from 'vue';
|
|||
import type { ParsedAiContent } from './useAiContentParsers';
|
||||
import { useAiContentParsers } from './useAiContentParsers';
|
||||
import VueMarkdown from 'vue-markdown-render';
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { NodeConnectionType, type IDataObject } from 'n8n-workflow';
|
||||
|
@ -78,13 +78,15 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const clipboard = useClipboard();
|
||||
const { showMessage } = useToast();
|
||||
const contentParsers = useAiContentParsers();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const isExpanded = ref(getInitialExpandedState());
|
||||
const isShowRaw = ref(false);
|
||||
const contentParsed = ref(false);
|
||||
const parsedRun = ref(undefined as ParsedAiContent | undefined);
|
||||
|
||||
function getInitialExpandedState() {
|
||||
const collapsedTypes = {
|
||||
input: [NodeConnectionType.AiDocument, NodeConnectionType.AiTextSplitter],
|
||||
|
@ -155,12 +157,9 @@ function onBlockHeaderClick() {
|
|||
isExpanded.value = !isExpanded.value;
|
||||
}
|
||||
|
||||
function copyToClipboard(content: IDataObject | IDataObject[]) {
|
||||
const copyToClipboardFn = useCopyToClipboard();
|
||||
const { showMessage } = useToast();
|
||||
|
||||
function onCopyToClipboard(content: IDataObject | IDataObject[]) {
|
||||
try {
|
||||
copyToClipboardFn(JSON.stringify(content, undefined, 2));
|
||||
void clipboard.copy(JSON.stringify(content, undefined, 2));
|
||||
showMessage({
|
||||
title: i18n.baseText('generic.copiedToClipboard'),
|
||||
type: 'success',
|
||||
|
|
|
@ -41,7 +41,6 @@ import { mapStores } from 'pinia';
|
|||
import jp from 'jsonpath';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { pinData } from '@/mixins/pinData';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { clearJsonKey, convertPath } from '@/utils/typesUtils';
|
||||
|
@ -52,6 +51,7 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
|||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { nonExistingJsonPath } from '@/constants';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
type JsonPathData = {
|
||||
path: string;
|
||||
|
@ -60,8 +60,7 @@ type JsonPathData = {
|
|||
|
||||
export default defineComponent({
|
||||
name: 'RunDataJsonActions',
|
||||
mixins: [genericHelpers, pinData, copyPaste],
|
||||
|
||||
mixins: [genericHelpers, pinData],
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<INodeUi>,
|
||||
|
@ -96,9 +95,12 @@ export default defineComponent({
|
|||
setup() {
|
||||
const i18n = useI18n();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
i18n,
|
||||
nodeHelpers,
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -222,7 +224,7 @@ export default defineComponent({
|
|||
in_execution_log: this.isReadOnlyRoute,
|
||||
});
|
||||
|
||||
this.copyToClipboard(value);
|
||||
void this.clipboard.copy(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -134,7 +134,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
@import '@/styles/css-animation-helpers.scss';
|
||||
@import '@/styles/variables';
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
|
|
|
@ -91,7 +91,7 @@ onUnmounted(() => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
@import '@/styles/css-animation-helpers.scss';
|
||||
@import '@/styles/variables';
|
||||
|
||||
.ioSearch {
|
||||
margin-right: var(--spacing-s);
|
||||
|
|
|
@ -117,16 +117,12 @@ import NodeExecuteButton from '@/components/NodeExecuteButton.vue';
|
|||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import CopyInput from '@/components/CopyInput.vue';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import type { N8nInfoAccordion } from 'n8n-design-system';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
type HelpRef = InstanceType<typeof N8nInfoAccordion>;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TriggerPanel',
|
||||
components: {
|
||||
|
@ -134,7 +130,7 @@ export default defineComponent({
|
|||
CopyInput,
|
||||
NodeIcon,
|
||||
},
|
||||
mixins: [workflowHelpers, copyPaste],
|
||||
mixins: [workflowHelpers],
|
||||
props: {
|
||||
nodeName: {
|
||||
type: String,
|
||||
|
|
|
@ -4,14 +4,14 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|||
import type { EnvironmentVariable, Rule, RuleGroup } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { EnterpriseEditionFeature } from '@/constants';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { getVariablesPermissions } from '@/permissions';
|
||||
|
||||
const i18n = useI18n();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const clipboard = useClipboard();
|
||||
const { showMessage } = useToast();
|
||||
const settingsStore = useSettingsStore();
|
||||
const usersStore = useUsersStore();
|
||||
|
@ -120,7 +120,7 @@ function onValidate(key: string, value: boolean) {
|
|||
}
|
||||
|
||||
function onUsageClick() {
|
||||
copyToClipboard(usage.value);
|
||||
void clipboard.copy(usage.value);
|
||||
showMessage({
|
||||
title: i18n.baseText('variables.row.usage.copiedToClipboard'),
|
||||
type: 'success',
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
v-for="item in props.items"
|
||||
:key="item.address"
|
||||
:class="$style.accordionItem"
|
||||
@click="copyToClipboard(item.address)"
|
||||
@click="onCopyToClipboard(item.address)"
|
||||
>
|
||||
{{ item.family }}: <span :class="$style.clickable">{{ item.address }}</span>
|
||||
{{ item.internal ? '(internal)' : '' }}
|
||||
|
@ -22,7 +22,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { IPushDataWorkerStatusPayload } from '@/Interface';
|
||||
import WorkerAccordion from './WorkerAccordion.ee.vue';
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
|
@ -31,13 +31,12 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const clipboard = useClipboard();
|
||||
const { showMessage } = useToast();
|
||||
|
||||
function copyToClipboard(content: string) {
|
||||
const copyToClipboardFn = useCopyToClipboard();
|
||||
const { showMessage } = useToast();
|
||||
|
||||
function onCopyToClipboard(content: string) {
|
||||
try {
|
||||
copyToClipboardFn(content);
|
||||
void clipboard.copy(content);
|
||||
showMessage({
|
||||
title: i18n.baseText('workerList.item.copyAddressToClipboard'),
|
||||
type: 'success',
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
const testValue = 'This is a test';
|
||||
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const pasted = ref('');
|
||||
const clipboard = useClipboard({
|
||||
onPaste(data) {
|
||||
pasted.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
return () =>
|
||||
h('div', [
|
||||
h('button', {
|
||||
'data-test-id': 'copy',
|
||||
onClick: () => {
|
||||
void clipboard.copy(testValue);
|
||||
},
|
||||
}),
|
||||
h('div', { 'data-test-id': 'paste' }, pasted.value),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
describe('useClipboard()', () => {
|
||||
beforeAll(() => {
|
||||
userEvent.setup();
|
||||
});
|
||||
|
||||
describe('copy()', () => {
|
||||
it('should copy text value', async () => {
|
||||
const { getByTestId } = render(TestComponent);
|
||||
|
||||
const copyButton = getByTestId('copy');
|
||||
copyButton.click();
|
||||
expect((window.navigator.clipboard as unknown as { items: string[] }).items).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClipboardPasteEvent()', () => {
|
||||
it('should trigger on clipboard paste event', async () => {
|
||||
const { getByTestId } = render(TestComponent);
|
||||
|
||||
const pasteElement = getByTestId('paste');
|
||||
await userEvent.paste(testValue);
|
||||
expect(pasteElement.textContent).toEqual(testValue);
|
||||
});
|
||||
});
|
||||
});
|
80
packages/editor-ui/src/composables/useClipboard.ts
Normal file
80
packages/editor-ui/src/composables/useClipboard.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useClipboard as useClipboardCore } from '@vueuse/core';
|
||||
|
||||
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
|
||||
|
||||
export function useClipboard(
|
||||
options: {
|
||||
onPaste: ClipboardEventFn;
|
||||
} = {
|
||||
onPaste() {},
|
||||
},
|
||||
) {
|
||||
const { copy, copied, isSupported, text } = useClipboardCore();
|
||||
|
||||
const ignoreClasses = ['el-messsage-box', 'ignore-key-press'];
|
||||
const initialized = ref(false);
|
||||
|
||||
const onPasteCallback = ref<ClipboardEventFn | null>(options.onPaste || null);
|
||||
|
||||
/**
|
||||
* Handles copy/paste events
|
||||
* @param event
|
||||
*/
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
if (!onPasteCallback.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the event got emitted from a message box or from something
|
||||
// else which should ignore the copy/paste
|
||||
const path = event.composedPath?.() as HTMLElement[];
|
||||
for (const pathElement of path) {
|
||||
if (
|
||||
pathElement.className &&
|
||||
ignoreClasses.some((className) => pathElement.className.includes?.(className))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (clipboardData !== null) {
|
||||
const clipboardValue = clipboardData.getData('text/plain');
|
||||
onPasteCallback.value(clipboardValue, event);
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedOnPaste = debounce(onPaste, 1000, { leading: true });
|
||||
|
||||
/**
|
||||
* Initialize copy/paste elements and events
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (initialized.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('paste', debouncedOnPaste);
|
||||
|
||||
initialized.value = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove copy/paste elements and events
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
if (initialized.value) {
|
||||
document.removeEventListener('paste', debouncedOnPaste);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
copy,
|
||||
copied,
|
||||
isSupported,
|
||||
text,
|
||||
onPaste: onPasteCallback,
|
||||
};
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import copyToClipboard from 'copy-to-clipboard';
|
||||
|
||||
export function useCopyToClipboard(): (text: string) => void {
|
||||
return copyToClipboard;
|
||||
}
|
|
@ -5,7 +5,6 @@ import '@jsplumb/browser-ui/css/jsplumbtoolkit.css';
|
|||
import 'n8n-design-system/css/index.scss';
|
||||
|
||||
import './n8n-theme.scss';
|
||||
import './styles/autocomplete-theme.scss';
|
||||
|
||||
import '@fontsource/open-sans/latin-400.css';
|
||||
import '@fontsource/open-sans/latin-600.css';
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
/**
|
||||
* Captures any pasted data and sends it to method "receivedCopyPasteData" which has to be
|
||||
* defined on the component which uses this mixin
|
||||
*/
|
||||
import { defineComponent } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
export const copyPaste = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
copyPasteElementsGotCreated: false,
|
||||
hiddenInput: null as null | Element,
|
||||
onPaste: null as null | Function,
|
||||
onBeforePaste: null as null | Function,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.copyPasteElementsGotCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copyPasteElementsGotCreated = true;
|
||||
// Define the style of the html elements that get created to make
|
||||
// sure that they are not visible
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = `
|
||||
.hidden-copy-paste {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: block;
|
||||
font-size: 1px;
|
||||
z-index: -1;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
padding: 0;
|
||||
resize: none;
|
||||
outline: none;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
`;
|
||||
document.getElementsByTagName('head')[0].appendChild(style);
|
||||
|
||||
// Code is mainly from
|
||||
// https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/
|
||||
const isSafari =
|
||||
navigator.appVersion.search('Safari') !== -1 &&
|
||||
navigator.appVersion.search('Chrome') === -1 &&
|
||||
navigator.appVersion.search('CrMo') === -1 &&
|
||||
navigator.appVersion.search('CriOS') === -1;
|
||||
const isIe =
|
||||
navigator.userAgent.toLowerCase().indexOf('msie') !== -1 ||
|
||||
navigator.userAgent.toLowerCase().indexOf('trident') !== -1;
|
||||
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.setAttribute('type', 'text');
|
||||
hiddenInput.setAttribute('id', 'hidden-input-copy-paste');
|
||||
hiddenInput.setAttribute('class', 'hidden-copy-paste');
|
||||
hiddenInput.setAttribute('data-test-id', 'hidden-copy-paste');
|
||||
this.hiddenInput = hiddenInput;
|
||||
|
||||
document.body.append(hiddenInput);
|
||||
|
||||
let ieClipboardDiv: HTMLDivElement | null = null;
|
||||
if (isIe) {
|
||||
ieClipboardDiv = document.createElement('div');
|
||||
ieClipboardDiv.setAttribute('id', 'hidden-ie-clipboard-copy-paste');
|
||||
ieClipboardDiv.setAttribute('class', 'hidden-copy-paste');
|
||||
ieClipboardDiv.setAttribute('contenteditable', 'true');
|
||||
document.body.append(ieClipboardDiv);
|
||||
|
||||
this.onBeforePaste = () => {
|
||||
// @ts-ignore
|
||||
if (hiddenInput.is(':focus')) {
|
||||
this.focusIeClipboardDiv(ieClipboardDiv as HTMLDivElement);
|
||||
}
|
||||
};
|
||||
// @ts-ignore
|
||||
document.addEventListener('beforepaste', this.onBeforePaste, true);
|
||||
}
|
||||
|
||||
let userInput = '';
|
||||
const hiddenInputListener = (text: string) => {};
|
||||
|
||||
hiddenInput.addEventListener('input', (e) => {
|
||||
const value = hiddenInput.value;
|
||||
userInput += value;
|
||||
hiddenInputListener(userInput);
|
||||
|
||||
// There is a bug (sometimes) with Safari and the input area can't be updated during
|
||||
// the input event, so we update the input area after the event is done being processed
|
||||
if (isSafari) {
|
||||
hiddenInput.focus();
|
||||
setTimeout(() => {
|
||||
this.focusHiddenArea(hiddenInput);
|
||||
}, 0);
|
||||
} else {
|
||||
this.focusHiddenArea(hiddenInput);
|
||||
}
|
||||
});
|
||||
|
||||
this.onPaste = debounce(
|
||||
(e) => {
|
||||
const event = 'paste';
|
||||
// Check if the event got emitted from a message box or from something
|
||||
// else which should ignore the copy/paste
|
||||
// @ts-ignore
|
||||
const path = e.path || e.composedPath?.();
|
||||
for (let index = 0; index < path.length; index++) {
|
||||
if (
|
||||
path[index].className &&
|
||||
typeof path[index].className === 'string' &&
|
||||
(path[index].className.includes('el-message-box') ||
|
||||
path[index].className.includes('ignore-key-press'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ieClipboardDiv !== null) {
|
||||
this.ieClipboardEvent(event, ieClipboardDiv);
|
||||
} else {
|
||||
this.standardClipboardEvent(event, e as ClipboardEvent);
|
||||
// @ts-ignore
|
||||
if (
|
||||
!document.activeElement ||
|
||||
(document.activeElement &&
|
||||
['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)
|
||||
) {
|
||||
// That it still allows to paste into text, email, password & textarea-fields we
|
||||
// check if we can identify the active element and if so only
|
||||
// run it if something else is selected.
|
||||
this.focusHiddenArea(hiddenInput);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
// Set clipboard event listeners on the document.
|
||||
// @ts-ignore
|
||||
document.addEventListener('paste', this.onPaste);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.remove();
|
||||
}
|
||||
if (this.onPaste) {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('paste', this.onPaste);
|
||||
}
|
||||
if (this.onBeforePaste) {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('beforepaste', this.onBeforePaste);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
receivedCopyPasteData(plainTextData: string, event?: ClipboardEvent): void {
|
||||
// THIS HAS TO BE DEFINED IN COMPONENT!
|
||||
},
|
||||
|
||||
// For every browser except IE, we can easily get and set data on the clipboard
|
||||
standardClipboardEvent(clipboardEventName: string, event: ClipboardEvent) {
|
||||
const clipboardData = event.clipboardData;
|
||||
if (clipboardData !== null && clipboardEventName === 'paste') {
|
||||
const clipboardText = clipboardData.getData('text/plain');
|
||||
this.receivedCopyPasteData(clipboardText, event);
|
||||
}
|
||||
},
|
||||
|
||||
// For IE, we can get/set Text or URL just as we normally would
|
||||
ieClipboardEvent(clipboardEventName: string, ieClipboardDiv: HTMLDivElement) {
|
||||
// @ts-ignore
|
||||
const clipboardData = window.clipboardData;
|
||||
if (clipboardEventName === 'paste') {
|
||||
const clipboardText = clipboardData.getData('Text');
|
||||
// @ts-ignore
|
||||
ieClipboardDiv.empty();
|
||||
this.receivedCopyPasteData(clipboardText);
|
||||
}
|
||||
},
|
||||
|
||||
// Focuses an element to be ready for copy/paste (used exclusively for IE)
|
||||
focusIeClipboardDiv(ieClipboardDiv: HTMLDivElement) {
|
||||
ieClipboardDiv.focus();
|
||||
const range = document.createRange();
|
||||
// @ts-ignore
|
||||
range.selectNodeContents(ieClipboardDiv.get(0));
|
||||
const selection = window.getSelection();
|
||||
if (selection !== null) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
},
|
||||
|
||||
focusHiddenArea(hiddenInput: HTMLInputElement) {
|
||||
// In order to ensure that the browser will fire clipboard events, we always need to have something selected
|
||||
hiddenInput.value = ' ';
|
||||
hiddenInput.focus();
|
||||
hiddenInput.select();
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies given data to clipboard
|
||||
*/
|
||||
copyToClipboard(value: string): void {
|
||||
// FROM: https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
|
||||
const element = document.createElement('textarea'); // Create a <textarea> element
|
||||
element.value = value; // Set its value to the string that you want copied
|
||||
element.setAttribute('readonly', ''); // Make it readonly to be tamper-proof
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = '-9999px'; // Move outside the screen to make it invisible
|
||||
document.body.appendChild(element); // Append the <textarea> element to the HTML document
|
||||
|
||||
const selection = document.getSelection();
|
||||
if (selection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected =
|
||||
selection.rangeCount > 0 // Check if there is any content selected previously
|
||||
? selection.getRangeAt(0) // Store selection if found
|
||||
: false; // Mark as false to know no selection existed before
|
||||
element.select(); // Select the <textarea> content
|
||||
document.execCommand('copy'); // Copy - only works as a result of a user action (e.g. click events)
|
||||
document.body.removeChild(element); // Remove the <textarea> element
|
||||
if (selected) {
|
||||
// If a selection existed before copying
|
||||
selection.removeAllRanges(); // Unselect everything on the HTML document
|
||||
selection.addRange(selected); // Restore the original selection
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
@import 'styles/plugins';
|
||||
|
||||
:root {
|
||||
--node-type-background-l: 95%;
|
||||
|
||||
|
|
|
@ -1031,10 +1031,10 @@
|
|||
"nodeView.cantExecuteNoTrigger": "Cannot execute workflow",
|
||||
"nodeView.canvasAddButton.addATriggerNodeBeforeExecuting": "Add a Trigger Node before executing the workflow",
|
||||
"nodeView.canvasAddButton.addFirstStep": "Add first step…",
|
||||
"nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText": "",
|
||||
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
||||
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
||||
"nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
||||
"nodeView.confirmMessage.onClipboardPasteEvent.cancelButtonText": "",
|
||||
"nodeView.confirmMessage.onClipboardPasteEvent.confirmButtonText": "Yes, import",
|
||||
"nodeView.confirmMessage.onClipboardPasteEvent.headline": "Import Workflow?",
|
||||
"nodeView.confirmMessage.onClipboardPasteEvent.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
||||
"nodeView.confirmMessage.debug.cancelButtonText": "Cancel",
|
||||
"nodeView.confirmMessage.debug.confirmButtonText": "Unpin",
|
||||
"nodeView.confirmMessage.debug.headline": "Unpin workflow data",
|
||||
|
|
1
packages/editor-ui/src/styles/plugins/index.scss
Normal file
1
packages/editor-ui/src/styles/plugins/index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import "codemirror";
|
|
@ -239,7 +239,6 @@ import {
|
|||
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
||||
TIME,
|
||||
} from '@/constants';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow';
|
||||
|
||||
|
@ -372,6 +371,7 @@ import { sourceControlEventBus } from '@/event-bus/source-control';
|
|||
import { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/nodeViewUtils';
|
||||
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
|
@ -394,15 +394,7 @@ export default defineComponent({
|
|||
CanvasControls,
|
||||
ContextMenu,
|
||||
},
|
||||
mixins: [
|
||||
copyPaste,
|
||||
genericHelpers,
|
||||
moveNodeWorkflow,
|
||||
workflowHelpers,
|
||||
workflowRun,
|
||||
debounceHelper,
|
||||
pinData,
|
||||
],
|
||||
mixins: [genericHelpers, moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper, pinData],
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
|
||||
|
@ -469,6 +461,7 @@ export default defineComponent({
|
|||
const contextMenu = useContextMenu();
|
||||
const dataSchema = useDataSchema();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
locale,
|
||||
|
@ -476,6 +469,7 @@ export default defineComponent({
|
|||
dataSchema,
|
||||
nodeHelpers,
|
||||
externalHooks,
|
||||
clipboard,
|
||||
...useCanvasMouseSelect(),
|
||||
...useGlobalLinkActions(),
|
||||
...useTitleChange(),
|
||||
|
@ -756,6 +750,8 @@ export default defineComponent({
|
|||
this.titleReset();
|
||||
window.addEventListener('message', this.onPostMessageReceived);
|
||||
|
||||
this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
||||
|
||||
this.startLoading();
|
||||
const loadPromises = [
|
||||
this.loadActiveWorkflows(),
|
||||
|
@ -1823,7 +1819,7 @@ export default defineComponent({
|
|||
|
||||
const nodeData = JSON.stringify(workflowToCopy, null, 2);
|
||||
|
||||
this.copyToClipboard(nodeData);
|
||||
this.clipboard.copy(nodeData);
|
||||
if (data.nodes.length > 0) {
|
||||
if (!isCut) {
|
||||
this.showMessage({
|
||||
|
@ -1928,7 +1924,7 @@ export default defineComponent({
|
|||
/**
|
||||
* This method gets called when data got pasted into the window
|
||||
*/
|
||||
async receivedCopyPasteData(plainTextData: string): Promise<void> {
|
||||
async onClipboardPasteEvent(plainTextData: string): Promise<void> {
|
||||
if (this.readOnlyEnv) {
|
||||
return;
|
||||
}
|
||||
|
@ -1948,17 +1944,17 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const importConfirm = await this.confirm(
|
||||
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.message', {
|
||||
this.$locale.baseText('nodeView.confirmMessage.onClipboardPasteEvent.message', {
|
||||
interpolate: { plainTextData },
|
||||
}),
|
||||
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.headline'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.onClipboardPasteEvent.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$locale.baseText(
|
||||
'nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText',
|
||||
'nodeView.confirmMessage.onClipboardPasteEvent.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: this.$locale.baseText(
|
||||
'nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText',
|
||||
'nodeView.confirmMessage.onClipboardPasteEvent.cancelButtonText',
|
||||
),
|
||||
dangerouslyUseHTMLString: true,
|
||||
},
|
||||
|
|
|
@ -240,7 +240,7 @@ const openPricingPage = () => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
@import '@/styles/css-animation-helpers.scss';
|
||||
@import '@/styles/variables';
|
||||
|
||||
.center > div {
|
||||
justify-content: center;
|
||||
|
|
|
@ -91,7 +91,6 @@ import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/consta
|
|||
|
||||
import type { IUser, IUserListAction } from '@/Interface';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
|
@ -99,12 +98,15 @@ import { useUsageStore } from '@/stores/usage.store';
|
|||
import { useSSOStore } from '@/stores/sso.store';
|
||||
import { hasPermission } from '@/rbac/permissions';
|
||||
import { ROLE } from '@/utils/userUtils';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingsUsersView',
|
||||
mixins: [copyPaste],
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
|
@ -222,7 +224,7 @@ export default defineComponent({
|
|||
async onCopyInviteLink(userId: string) {
|
||||
const user = this.usersStore.getUserById(userId);
|
||||
if (user?.inviteAcceptUrl) {
|
||||
this.copyToClipboard(user.inviteAcceptUrl);
|
||||
void this.clipboard.copy(user.inviteAcceptUrl);
|
||||
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
|
@ -235,7 +237,7 @@ export default defineComponent({
|
|||
const user = this.usersStore.getUserById(userId);
|
||||
if (user) {
|
||||
const url = await this.usersStore.getUserPasswordResetLink(user);
|
||||
this.copyToClipboard(url.link);
|
||||
void this.clipboard.copy(url.link);
|
||||
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
|
|
|
@ -1035,9 +1035,6 @@ importers:
|
|||
codemirror-lang-n8n-expression:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0)
|
||||
copy-to-clipboard:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
dateformat:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
|
@ -13362,12 +13359,6 @@ packages:
|
|||
is-plain-object: 5.0.0
|
||||
dev: true
|
||||
|
||||
/copy-to-clipboard@3.3.3:
|
||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||
dependencies:
|
||||
toggle-selection: 1.0.6
|
||||
dev: false
|
||||
|
||||
/core-js-compat@3.32.0:
|
||||
resolution: {integrity: sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==}
|
||||
dependencies:
|
||||
|
@ -24525,10 +24516,6 @@ packages:
|
|||
resolution: {integrity: sha512-vXk8htr8mIl3hc2s2mDkaPTBfqmqZA2o0x7eXbxUibdrpEIPdpM0L9hH/RvEvlgSM+ZTgS34sGipk5+VrLJCLA==}
|
||||
dev: true
|
||||
|
||||
/toggle-selection@1.0.6:
|
||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||
dev: false
|
||||
|
||||
/toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
Loading…
Reference in a new issue