mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(editor): Reka UI inline text edit component (#15752)
This commit is contained in:
@@ -68,8 +68,7 @@ export function getCredentialSaveButton() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function setCredentialName(name: string) {
|
export function setCredentialName(name: string) {
|
||||||
cy.getByTestId('credential-name').click();
|
cy.getByTestId('credential-name').find('span[data-test-id=inline-edit-preview]').click();
|
||||||
cy.getByTestId('credential-name').find('input').clear();
|
|
||||||
cy.getByTestId('credential-name').type(name);
|
cy.getByTestId('credential-name').type(name);
|
||||||
}
|
}
|
||||||
export function saveCredential() {
|
export function saveCredential() {
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ export function renameFolderFromListActions(folderName: string, newName: string)
|
|||||||
getListActionsToggle().click();
|
getListActionsToggle().click();
|
||||||
getListActionItem('rename').click();
|
getListActionItem('rename').click();
|
||||||
getInlineEditInput().should('be.visible');
|
getInlineEditInput().should('be.visible');
|
||||||
getInlineEditInput().type(newName, { delay: 50 });
|
getInlineEditInput().type(`${newName}{enter}`, { delay: 50 });
|
||||||
successToast().should('exist');
|
successToast().should('exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ describe('Log Streaming Settings', () => {
|
|||||||
|
|
||||||
settingsLogStreamingPage.getters
|
settingsLogStreamingPage.getters
|
||||||
.getDestinationNameInput()
|
.getDestinationNameInput()
|
||||||
.find('input')
|
.find('span[data-test-id=inline-edit-preview]')
|
||||||
.clear()
|
.click();
|
||||||
.type('Destination 0');
|
cy.getByTestId('inline-edit-input').type('Destination 0');
|
||||||
settingsLogStreamingPage.getters.getDestinationSaveButton().click();
|
settingsLogStreamingPage.getters.getDestinationSaveButton().click();
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
getVisibleModalOverlay().click(1, 1);
|
getVisibleModalOverlay().click(1, 1);
|
||||||
@@ -84,9 +84,9 @@ describe('Log Streaming Settings', () => {
|
|||||||
settingsLogStreamingPage.getters.getDestinationNameInput().click();
|
settingsLogStreamingPage.getters.getDestinationNameInput().click();
|
||||||
settingsLogStreamingPage.getters
|
settingsLogStreamingPage.getters
|
||||||
.getDestinationNameInput()
|
.getDestinationNameInput()
|
||||||
.find('input')
|
.find('span[data-test-id=inline-edit-preview]')
|
||||||
.clear()
|
.click();
|
||||||
.type('Destination 1');
|
cy.getByTestId('inline-edit-input').type('Destination 1');
|
||||||
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled');
|
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled');
|
||||||
settingsLogStreamingPage.getters.getDestinationSaveButton().click();
|
settingsLogStreamingPage.getters.getDestinationSaveButton().click();
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export class CredentialsModal extends BasePage {
|
|||||||
connectionParameter: (fieldName: string) =>
|
connectionParameter: (fieldName: string) =>
|
||||||
this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`),
|
this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`),
|
||||||
name: () => cy.getByTestId('credential-name'),
|
name: () => cy.getByTestId('credential-name'),
|
||||||
|
namePreview: () =>
|
||||||
|
cy.getByTestId('credential-name').find('span[data-test-id=inline-edit-preview]'),
|
||||||
nameInput: () => cy.getByTestId('credential-name').find('input'),
|
nameInput: () => cy.getByTestId('credential-name').find('input'),
|
||||||
deleteButton: () => cy.getByTestId('credential-delete-button'),
|
deleteButton: () => cy.getByTestId('credential-delete-button'),
|
||||||
closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(),
|
closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(),
|
||||||
@@ -43,7 +45,7 @@ export class CredentialsModal extends BasePage {
|
|||||||
getVisibleSelect().contains(email.toLowerCase()).click();
|
getVisibleSelect().contains(email.toLowerCase()).click();
|
||||||
},
|
},
|
||||||
setName: (name: string) => {
|
setName: (name: string) => {
|
||||||
this.getters.name().click();
|
this.getters.name().getByTestId('inline-edit-preview').click();
|
||||||
this.getters.nameInput().clear().type(name);
|
this.getters.nameInput().clear().type(name);
|
||||||
},
|
},
|
||||||
save: (test = false) => {
|
save: (test = false) => {
|
||||||
@@ -93,7 +95,7 @@ export class CredentialsModal extends BasePage {
|
|||||||
this.actions.fillCredentialsForm(closeModal);
|
this.actions.fillCredentialsForm(closeModal);
|
||||||
},
|
},
|
||||||
renameCredential: (newName: string) => {
|
renameCredential: (newName: string) => {
|
||||||
this.getters.nameInput().type('{selectall}');
|
this.getters.namePreview().click();
|
||||||
this.getters.nameInput().type(newName);
|
this.getters.nameInput().type(newName);
|
||||||
this.getters.nameInput().type('{enter}');
|
this.getters.nameInput().type('{enter}');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export const getters = {
|
|||||||
skipLink: () => cy.get('a:contains("Skip")'),
|
skipLink: () => cy.get('a:contains("Skip")'),
|
||||||
title: (title: string) => cy.get(`h1:contains(${title})`),
|
title: (title: string) => cy.get(`h1:contains(${title})`),
|
||||||
infoCallout: () => cy.getByTestId('info-callout'),
|
infoCallout: () => cy.getByTestId('info-callout'),
|
||||||
|
|
||||||
|
namePreview: () =>
|
||||||
|
cy.getByTestId('credential-name').find('span[data-test-id=inline-edit-preview]'),
|
||||||
|
nameInput: () => cy.getByTestId('credential-name').find('input'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
||||||
@@ -22,7 +26,8 @@ export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
|||||||
*/
|
*/
|
||||||
export const fillInDummyCredentialsForApp = (appName: string) => {
|
export const fillInDummyCredentialsForApp = (appName: string) => {
|
||||||
formStep.getCreateAppCredentialsButton(appName).click();
|
formStep.getCreateAppCredentialsButton(appName).click();
|
||||||
credentialsModal.getters.editCredentialModal().find('input:first()').type('test');
|
credentialsModal.getters.namePreview().click();
|
||||||
|
credentialsModal.getters.nameInput().type('test');
|
||||||
credentialsModal.actions.save(false);
|
credentialsModal.actions.save(false);
|
||||||
credentialsModal.actions.close();
|
credentialsModal.actions.close();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ export class WorkflowPage extends BasePage {
|
|||||||
cy.get('body').type('{del}');
|
cy.get('body').type('{del}');
|
||||||
},
|
},
|
||||||
setWorkflowName: (name: string) => {
|
setWorkflowName: (name: string) => {
|
||||||
this.getters.workflowNameInput().should('be.disabled');
|
|
||||||
this.getters.workflowNameInput().parent().click();
|
this.getters.workflowNameInput().parent().click();
|
||||||
this.getters.workflowNameInput().should('be.enabled');
|
this.getters.workflowNameInput().should('be.enabled');
|
||||||
this.getters.workflowNameInput().clear().type(name).type('{enter}');
|
this.getters.workflowNameInput().clear().type(name).type('{enter}');
|
||||||
|
|||||||
@@ -19,3 +19,9 @@
|
|||||||
body {
|
body {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@n8n/eslint-config": "workspace:*",
|
"@n8n/eslint-config": "workspace:*",
|
||||||
|
"@n8n/storybook": "workspace:*",
|
||||||
"@n8n/typescript-config": "workspace:*",
|
"@n8n/typescript-config": "workspace:*",
|
||||||
"@n8n/vitest-config": "workspace:*",
|
"@n8n/vitest-config": "workspace:*",
|
||||||
"@n8n/storybook": "workspace:*",
|
|
||||||
"@testing-library/jest-dom": "catalog:frontend",
|
"@testing-library/jest-dom": "catalog:frontend",
|
||||||
"@testing-library/user-event": "catalog:frontend",
|
"@testing-library/user-event": "catalog:frontend",
|
||||||
"@testing-library/vue": "catalog:frontend",
|
"@testing-library/vue": "catalog:frontend",
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
"vue-tsc": "catalog:frontend"
|
"vue-tsc": "catalog:frontend"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@n8n/composables": "workspace:*",
|
|
||||||
"@n8n/utils": "workspace:*",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
|
"@n8n/composables": "workspace:*",
|
||||||
|
"@n8n/utils": "workspace:*",
|
||||||
"@tanstack/vue-table": "^8.21.2",
|
"@tanstack/vue-table": "^8.21.2",
|
||||||
"element-plus": "catalog:frontend",
|
"element-plus": "catalog:frontend",
|
||||||
"is-emoji-supported": "^0.0.5",
|
"is-emoji-supported": "^0.0.5",
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
"markdown-it-link-attributes": "^4.0.1",
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
"markdown-it-task-lists": "^2.1.1",
|
"markdown-it-task-lists": "^2.1.1",
|
||||||
"parse-diff": "^0.11.1",
|
"parse-diff": "^0.11.1",
|
||||||
|
"reka-ui": "^2.2.1",
|
||||||
"sanitize-html": "2.12.1",
|
"sanitize-html": "2.12.1",
|
||||||
"vue": "catalog:frontend",
|
"vue": "catalog:frontend",
|
||||||
"vue-boring-avatars": "^1.3.0",
|
"vue-boring-avatars": "^1.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { StoryFn } from '@storybook/vue3';
|
||||||
|
|
||||||
|
import InlineTextEdit from './InlineTextEdit.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/InlineTextEdit',
|
||||||
|
component: InlineTextEdit,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template: StoryFn = (args, { argTypes }) => ({
|
||||||
|
setup: () => ({ args }),
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
InlineTextEdit,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="story">
|
||||||
|
<N8nInlineTextEdit v-bind="args" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const primary = Template.bind({});
|
||||||
|
primary.args = {
|
||||||
|
modelValue: 'Test',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const placeholder = Template.bind({});
|
||||||
|
placeholder.args = {
|
||||||
|
modelValue: '',
|
||||||
|
placeholder: 'Enter workflow name',
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { createComponentRenderer } from '@n8n/design-system/__tests__/render';
|
||||||
|
|
||||||
|
import N8nInlineTextEdit from './InlineTextEdit.vue';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(N8nInlineTextEdit);
|
||||||
|
|
||||||
|
describe('N8nInlineTextEdit', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const wrapper = renderComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: 'Test Value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('value should update on enter', async () => {
|
||||||
|
const wrapper = renderComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: 'Test Value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const input = wrapper.getByTestId('inline-edit-input');
|
||||||
|
const preview = wrapper.getByTestId('inline-edit-preview');
|
||||||
|
|
||||||
|
await wrapper.rerender({ modelValue: 'New Value' });
|
||||||
|
await userEvent.type(input, 'Updated Value');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
expect(preview).toHaveTextContent('Updated Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update value on blur if input is empty', async () => {
|
||||||
|
const wrapper = renderComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: 'Test Value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const input = wrapper.getByTestId('inline-edit-input');
|
||||||
|
const preview = wrapper.getByTestId('inline-edit-preview');
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.tab(); // Simulate blur
|
||||||
|
|
||||||
|
expect(preview).toHaveTextContent('Test Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update on escape key press', async () => {
|
||||||
|
const wrapper = renderComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: 'Test Value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const input = wrapper.getByTestId('inline-edit-input');
|
||||||
|
const preview = wrapper.getByTestId('inline-edit-preview');
|
||||||
|
|
||||||
|
await userEvent.type(input, 'Updated Value');
|
||||||
|
await userEvent.keyboard('{Escape}');
|
||||||
|
|
||||||
|
expect(preview).toHaveTextContent('Test Value');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { EditableArea, EditableInput, EditablePreview, EditableRoot } from 'reka-ui';
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
modelValue: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
maxLength?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 100,
|
||||||
|
maxWidth: 200,
|
||||||
|
minWidth: 64,
|
||||||
|
placeholder: 'Enter text...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:model-value': [value: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const editableRoot = useTemplateRef('editableRoot');
|
||||||
|
|
||||||
|
function forceFocus() {
|
||||||
|
if (editableRoot.value && !props.readOnly) {
|
||||||
|
editableRoot.value.edit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceCancel() {
|
||||||
|
if (editableRoot.value) {
|
||||||
|
newValue.value = props.modelValue;
|
||||||
|
editableRoot.value.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ forceFocus, forceCancel });
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
if (newValue.value === '') {
|
||||||
|
newValue.value = props.modelValue;
|
||||||
|
temp.value = props.modelValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:model-value', newValue.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(value: string) {
|
||||||
|
newValue.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStateChange(state: string) {
|
||||||
|
if (state === 'cancel') {
|
||||||
|
temp.value = newValue.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize logic
|
||||||
|
const newValue = ref(props.modelValue);
|
||||||
|
const temp = ref(props.modelValue || props.placeholder);
|
||||||
|
|
||||||
|
const measureSpan = useTemplateRef('measureSpan');
|
||||||
|
const { width: measuredWidth } = useElementSize(measureSpan);
|
||||||
|
|
||||||
|
const inputWidth = computed(() => {
|
||||||
|
return Math.max(props.minWidth, Math.min(measuredWidth.value + 1, props.maxWidth));
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange(event: Event) {
|
||||||
|
const { value } = event.target as HTMLInputElement;
|
||||||
|
const processedValue = value.replace(/\s/g, '.');
|
||||||
|
temp.value = processedValue.trim() !== '' ? processedValue : props.placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedInlineStyles = computed(() => {
|
||||||
|
return {
|
||||||
|
width: `${inputWidth.value}px`,
|
||||||
|
maxWidth: `${props.maxWidth}px`,
|
||||||
|
zIndex: 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EditableRoot
|
||||||
|
ref="editableRoot"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:model-value="newValue"
|
||||||
|
submit-mode="both"
|
||||||
|
:class="$style.inlineRenameRoot"
|
||||||
|
:title="modelValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
:max-length="maxLength"
|
||||||
|
:readonly="readOnly"
|
||||||
|
select-on-focus
|
||||||
|
auto-resize
|
||||||
|
@submit="onSubmit"
|
||||||
|
@update:model-value="onInput"
|
||||||
|
@update:state="onStateChange"
|
||||||
|
>
|
||||||
|
<EditableArea
|
||||||
|
:style="computedInlineStyles"
|
||||||
|
:class="$style.inlineRenameArea"
|
||||||
|
@click="forceFocus"
|
||||||
|
>
|
||||||
|
<span ref="measureSpan" :class="$style.measureSpan">
|
||||||
|
{{ temp }}
|
||||||
|
</span>
|
||||||
|
<EditablePreview
|
||||||
|
data-test-id="inline-edit-preview"
|
||||||
|
:class="$style.inlineRenamePreview"
|
||||||
|
:style="computedInlineStyles"
|
||||||
|
/>
|
||||||
|
<EditableInput
|
||||||
|
ref="input"
|
||||||
|
:class="$style.inlineRenameInput"
|
||||||
|
data-test-id="inline-edit-input"
|
||||||
|
:style="computedInlineStyles"
|
||||||
|
@input="onChange"
|
||||||
|
/>
|
||||||
|
</EditableArea>
|
||||||
|
</EditableRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.inlineRenameArea {
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--spacing-4xs) * -1);
|
||||||
|
left: calc(var(--spacing-3xs) * -1);
|
||||||
|
width: calc(100% + var(--spacing-xs));
|
||||||
|
height: calc(100% + var(--spacing-2xs));
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
background-color: var(--color-foreground-xlight);
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 0;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-focused],
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
border: 1px solid var(--color-foreground-base);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-focused] {
|
||||||
|
cursor: text;
|
||||||
|
&::after {
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineRenameArea[data-readonly] {
|
||||||
|
pointer-events: none;
|
||||||
|
&::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineRenamePreview {
|
||||||
|
width: fit-content;
|
||||||
|
transform: translateY(1.5px);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.measureSpan {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`N8nInlineTextEdit > should render correctly 1`] = `
|
||||||
|
"<div class="inlineRenameRoot" title="Test Value" dir="ltr" data-dismissable-layer="">
|
||||||
|
<div data-placeholder-shown="" style="display: inline-grid; width: 64px; max-width: 200px; z-index: 1;" class="inlineRenameArea"><span class="measureSpan">Test Value</span><span tabindex="0" data-placeholder-shown="" style="white-space: pre; user-select: none; grid-area: 1 / 1 / auto / auto; overflow: hidden; text-overflow: ellipsis; width: 64px; max-width: 200px; z-index: 1;" data-test-id="inline-edit-preview" class="inlineRenamePreview">Test Value</span><input placeholder="Enter text..." maxlength="100" aria-label="editable input" style="all: unset; grid-area: 1 / 1 / auto / auto; visibility: hidden; width: 64px; max-width: 200px; z-index: 1;" class="inlineRenameInput" data-test-id="inline-edit-input" value="Test Value"></div>
|
||||||
|
<!---->
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import InlineRename from './InlineTextEdit.vue';
|
||||||
|
|
||||||
|
export default InlineRename;
|
||||||
@@ -60,3 +60,4 @@ export { default as N8nIconPicker } from './N8nIconPicker';
|
|||||||
export { default as N8nBreadcrumbs } from './N8nBreadcrumbs';
|
export { default as N8nBreadcrumbs } from './N8nBreadcrumbs';
|
||||||
export { default as N8nTableBase } from './TableBase';
|
export { default as N8nTableBase } from './TableBase';
|
||||||
export { default as N8nDataTableServer } from './N8nDataTableServer';
|
export { default as N8nDataTableServer } from './N8nDataTableServer';
|
||||||
|
export { default as N8nInlineTextEdit } from './N8nInlineTextEdit';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ICredentialsDecryptedResponse,
|
ICredentialsDecryptedResponse,
|
||||||
@@ -22,7 +22,6 @@ import { NodeHelpers } from 'n8n-workflow';
|
|||||||
import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
|
import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
|
||||||
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
|
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
|
||||||
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
|
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
|
||||||
import InlineNameEdit from '@/components/InlineNameEdit.vue';
|
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from '@/components/Modal.vue';
|
||||||
import SaveButton from '@/components/SaveButton.vue';
|
import SaveButton from '@/components/SaveButton.vue';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
@@ -37,7 +36,7 @@ import { useSettingsStore } from '@/stores/settings.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 { Project, ProjectSharingData } from '@/types/projects.types';
|
import type { Project, ProjectSharingData } from '@/types/projects.types';
|
||||||
import type { IMenuItem } from '@n8n/design-system';
|
import { N8nInlineTextEdit, N8nText, type IMenuItem } from '@n8n/design-system';
|
||||||
import { assert } from '@n8n/utils/assert';
|
import { assert } from '@n8n/utils/assert';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
|
|
||||||
@@ -52,6 +51,7 @@ import {
|
|||||||
updateNodeAuthType,
|
updateNodeAuthType,
|
||||||
} from '@/utils/nodeTypesUtils';
|
} from '@/utils/nodeTypesUtils';
|
||||||
import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards';
|
import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modalName: string;
|
modalName: string;
|
||||||
@@ -1065,6 +1065,9 @@ function resetCredentialData(): void {
|
|||||||
homeProject,
|
homeProject,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const credNameRef = useTemplateRef('credNameRef');
|
||||||
|
const { width } = useElementSize(credNameRef);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1083,16 +1086,21 @@ function resetCredentialData(): void {
|
|||||||
<div :class="$style.credIcon">
|
<div :class="$style.credIcon">
|
||||||
<CredentialIcon :credential-type-name="defaultCredentialTypeName" />
|
<CredentialIcon :credential-type-name="defaultCredentialTypeName" />
|
||||||
</div>
|
</div>
|
||||||
<InlineNameEdit
|
<div ref="credNameRef" :class="$style.credName">
|
||||||
:model-value="credentialName"
|
<N8nInlineTextEdit
|
||||||
:subtitle="credentialType ? credentialType.displayName : ''"
|
v-if="credentialName"
|
||||||
:readonly="
|
data-test-id="credential-name"
|
||||||
!credentialPermissions.update || !credentialType || isEditingManagedCredential
|
:model-value="credentialName"
|
||||||
"
|
:max-width="width - 10"
|
||||||
type="Credential"
|
:readonly="
|
||||||
data-test-id="credential-name"
|
!credentialPermissions.update || !credentialType || isEditingManagedCredential
|
||||||
@update:model-value="onNameEdit"
|
"
|
||||||
/>
|
@update:model-value="onNameEdit"
|
||||||
|
/>
|
||||||
|
<N8nText v-if="credentialType" size="small" tag="p" color="text-light">{{
|
||||||
|
credentialType.displayName
|
||||||
|
}}</N8nText>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.credActions">
|
<div :class="$style.credActions">
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
@@ -1202,6 +1210,13 @@ function resetCredentialData(): void {
|
|||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credName {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
max-width: 170px;
|
max-width: 170px;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
modelValue: string;
|
|
||||||
placeholder?: string;
|
|
||||||
staticSize?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), { staticSize: false, placeholder: '' });
|
|
||||||
|
|
||||||
const hiddenValue = computed(() => {
|
|
||||||
let value = props.modelValue.replace(/\s/g, '.'); // force input to expand on space chars
|
|
||||||
if (!value) {
|
|
||||||
value = props.placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value}`; // adjust for padding
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- mock el-input element to apply styles -->
|
|
||||||
<div :class="{ 'el-input': true, 'static-size': staticSize }" :data-value="hiddenValue">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.el-input {
|
|
||||||
display: inline-grid;
|
|
||||||
font: inherit;
|
|
||||||
|
|
||||||
:deep(input) {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: var(--spacing-3xs) calc(var(--spacing-3xs) - 2px); // -2px for borders
|
|
||||||
width: 100%;
|
|
||||||
grid-area: 1 / 2;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
grid-area: 1 / 2;
|
|
||||||
font: inherit;
|
|
||||||
content: attr(data-value) ' ';
|
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: var(--spacing-3xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.static-size)::after {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
:deep(input):not(:focus) {
|
|
||||||
border: 1px solid var(--color-text-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(input):focus {
|
|
||||||
border: 1px solid var(--color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
||||||
import ExpandableInputBase from './ExpandableInputBase.vue';
|
|
||||||
import { onClickOutside } from '@vueuse/core';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
modelValue: string;
|
|
||||||
placeholder: string;
|
|
||||||
maxlength?: number;
|
|
||||||
autofocus?: boolean;
|
|
||||||
eventBus?: EventBus;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:model-value': [value: string];
|
|
||||||
enter: [value: string];
|
|
||||||
blur: [value: string];
|
|
||||||
esc: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement>();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// autofocus on input element is not reliable
|
|
||||||
if (props.autofocus && inputRef.value) {
|
|
||||||
focus();
|
|
||||||
}
|
|
||||||
props.eventBus?.on('focus', focus);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
props.eventBus?.off('focus', focus);
|
|
||||||
});
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
if (inputRef.value) {
|
|
||||||
inputRef.value.focus();
|
|
||||||
inputRef.value.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInput() {
|
|
||||||
if (inputRef.value) {
|
|
||||||
emit('update:model-value', inputRef.value.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEnter() {
|
|
||||||
if (inputRef.value) {
|
|
||||||
emit('enter', inputRef.value.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickOutside(inputRef, () => {
|
|
||||||
if (inputRef.value) {
|
|
||||||
emit('blur', inputRef.value.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function onEscape() {
|
|
||||||
emit('esc');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ExpandableInputBase :model-value="modelValue" :placeholder="placeholder">
|
|
||||||
<input
|
|
||||||
ref="inputRef"
|
|
||||||
:class="['el-input__inner', $style.input]"
|
|
||||||
:value="modelValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:maxlength="maxlength"
|
|
||||||
size="4"
|
|
||||||
@input="onInput"
|
|
||||||
@keydown.enter="onEnter"
|
|
||||||
@keydown.esc="onEscape"
|
|
||||||
/>
|
|
||||||
</ExpandableInputBase>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style module lang="scss">
|
|
||||||
.input {
|
|
||||||
padding: var(--spacing-4xs);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import ExpandableInputBase from './ExpandableInputBase.vue';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
modelValue: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineProps<Props>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ExpandableInputBase :model-value="modelValue" :static-size="true">
|
|
||||||
<input class="clickable preview" :value="modelValue" :disabled="true" size="4" />
|
|
||||||
</ExpandableInputBase>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
input.preview {
|
|
||||||
padding: var(--spacing-4xs);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview,
|
|
||||||
.preview:hover {
|
|
||||||
background-color: unset;
|
|
||||||
transition: unset;
|
|
||||||
pointer-events: none; // fix firefox bug
|
|
||||||
}
|
|
||||||
|
|
||||||
input[disabled] {
|
|
||||||
color: $custom-font-black;
|
|
||||||
|
|
||||||
// override safari colors
|
|
||||||
-webkit-text-fill-color: $custom-font-black;
|
|
||||||
-webkit-opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue';
|
|
||||||
import ExpandableInputPreview from '@/components/ExpandableInput/ExpandableInputPreview.vue';
|
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
isEditEnabled: boolean;
|
|
||||||
modelValue: string;
|
|
||||||
placeholder: string;
|
|
||||||
maxLength: number;
|
|
||||||
previewValue: string;
|
|
||||||
disabled: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
isEditEnabled: false,
|
|
||||||
modelValue: '',
|
|
||||||
placeholder: '',
|
|
||||||
maxLength: 0,
|
|
||||||
previewValue: '',
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
toggle: [];
|
|
||||||
submit: [payload: { name: string; onSubmit: (updated: boolean) => void }];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isDisabled = ref(props.disabled);
|
|
||||||
const newValue = ref('');
|
|
||||||
const escPressed = ref(false);
|
|
||||||
const inputBus = ref(createEventBus());
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.disabled,
|
|
||||||
(value) => {
|
|
||||||
isDisabled.value = value;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(value) => {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
newValue.value = value;
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
function onInput(val: string) {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
newValue.value = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClick() {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
newValue.value = props.modelValue;
|
|
||||||
emit('toggle');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBlur() {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
if (!escPressed.value) {
|
|
||||||
submit();
|
|
||||||
}
|
|
||||||
escPressed.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
const onSubmit = (updated: boolean) => {
|
|
||||||
isDisabled.value = false;
|
|
||||||
if (!updated) {
|
|
||||||
inputBus.value.emit('focus');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
isDisabled.value = true;
|
|
||||||
emit('submit', { name: newValue.value, onSubmit });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEscape() {
|
|
||||||
if (isDisabled.value) return;
|
|
||||||
escPressed.value = true;
|
|
||||||
emit('toggle');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span :class="$style['inline-edit']" @keydown.stop>
|
|
||||||
<span v-if="isEditEnabled && !isDisabled">
|
|
||||||
<ExpandableInputEdit
|
|
||||||
v-model="newValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:maxlength="maxLength"
|
|
||||||
:autofocus="true"
|
|
||||||
:event-bus="inputBus"
|
|
||||||
data-test-id="inline-edit-input"
|
|
||||||
@update:model-value="onInput"
|
|
||||||
@esc="onEscape"
|
|
||||||
@blur="onBlur"
|
|
||||||
@enter="submit"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else :class="$style.preview" @click="onClick">
|
|
||||||
<ExpandableInputPreview :model-value="previewValue || modelValue" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
/* Magic numbers here but this keeps preview and this input vertically aligned */
|
|
||||||
.inline-edit {
|
|
||||||
display: block;
|
|
||||||
height: 25px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
display: block;
|
|
||||||
height: 27px;
|
|
||||||
min-height: 27px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -135,8 +135,7 @@ describe('WorkflowDetails', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workflowName = getByTestId('workflow-name-input');
|
const workflowNameInput = getByTestId('inline-edit-input');
|
||||||
const workflowNameInput = workflowName.querySelector('input');
|
|
||||||
|
|
||||||
expect(workflowNameInput).toHaveValue('Test Workflow');
|
expect(workflowNameInput).toHaveValue('Test Workflow');
|
||||||
expect(getByText('tag1')).toBeInTheDocument();
|
expect(getByText('tag1')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
|
|
||||||
import InlineTextEdit from '@/components/InlineTextEdit.vue';
|
|
||||||
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
|
|
||||||
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
|
|
||||||
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
|
|
||||||
import SaveButton from '@/components/SaveButton.vue';
|
|
||||||
import ShortenName from '@/components/ShortenName.vue';
|
|
||||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
|
||||||
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
|
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
|
||||||
import {
|
import {
|
||||||
DUPLICATE_MODAL_KEY,
|
DUPLICATE_MODAL_KEY,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
@@ -23,6 +13,14 @@ import {
|
|||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
|
||||||
|
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
|
||||||
|
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||||
|
import SaveButton from '@/components/SaveButton.vue';
|
||||||
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
|
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
|
||||||
|
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
|
||||||
|
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
|
||||||
import { ResourceType } from '@/utils/projects.utils';
|
import { ResourceType } from '@/utils/projects.utils';
|
||||||
|
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
@@ -34,6 +32,18 @@ import { useUsersStore } from '@/stores/users.store';
|
|||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
|
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
import { useMessage } from '@/composables/useMessage';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { getResourcePermissions } from '@/permissions';
|
||||||
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { computed, ref, useCssModule, useTemplateRef, watch } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
ActionDropdownItem,
|
ActionDropdownItem,
|
||||||
FolderShortInfo,
|
FolderShortInfo,
|
||||||
@@ -41,26 +51,14 @@ import type {
|
|||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowToShare,
|
IWorkflowToShare,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
|
||||||
import { useMessage } from '@/composables/useMessage';
|
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { N8nInlineTextEdit } from '@n8n/design-system';
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
|
||||||
import { getResourcePermissions } from '@/permissions';
|
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
|
||||||
import { useFoldersStore } from '@/stores/folders.store';
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||||
|
import { type BaseTextKey, useI18n } from '@n8n/i18n';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
|
||||||
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
|
|
||||||
import type { BaseTextKey } from '@n8n/i18n';
|
|
||||||
import { useI18n } from '@n8n/i18n';
|
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { computed, ref, useCssModule, watch } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -101,7 +99,6 @@ const workflowHelpers = useWorkflowHelpers({ router });
|
|||||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
|
|
||||||
const isTagsEditEnabled = ref(false);
|
const isTagsEditEnabled = ref(false);
|
||||||
const isNameEditEnabled = ref(false);
|
|
||||||
const appliedTagIds = ref<string[]>([]);
|
const appliedTagIds = ref<string[]>([]);
|
||||||
const tagsSaving = ref(false);
|
const tagsSaving = ref(false);
|
||||||
const importFileRef = ref<HTMLInputElement | undefined>();
|
const importFileRef = ref<HTMLInputElement | undefined>();
|
||||||
@@ -267,7 +264,7 @@ watch(
|
|||||||
() => props.id,
|
() => props.id,
|
||||||
() => {
|
() => {
|
||||||
isTagsEditEnabled.value = false;
|
isTagsEditEnabled.value = false;
|
||||||
isNameEditEnabled.value = false;
|
renameInput.value?.forceCancel();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -332,7 +329,7 @@ function onTagsEditEnable() {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// allow name update to occur before disabling name edit
|
// allow name update to occur before disabling name edit
|
||||||
isNameEditEnabled.value = false;
|
renameInput.value?.forceCancel();
|
||||||
tagsEventBus.emit('focus');
|
tagsEventBus.emit('focus');
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
@@ -366,24 +363,14 @@ function onTagsEditEsc() {
|
|||||||
isTagsEditEnabled.value = false;
|
isTagsEditEnabled.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renameInput = useTemplateRef('renameInput');
|
||||||
function onNameToggle() {
|
function onNameToggle() {
|
||||||
isNameEditEnabled.value = !isNameEditEnabled.value;
|
if (renameInput.value?.forceFocus) {
|
||||||
if (isNameEditEnabled.value) {
|
renameInput.value.forceFocus();
|
||||||
if (isTagsEditEnabled.value) {
|
|
||||||
void onTagsBlur();
|
|
||||||
}
|
|
||||||
|
|
||||||
isTagsEditEnabled.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onNameSubmit({
|
async function onNameSubmit(name: string) {
|
||||||
name,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
onSubmit: (saved: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const newName = name.trim();
|
const newName = name.trim();
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
toast.showMessage({
|
toast.showMessage({
|
||||||
@@ -392,14 +379,12 @@ async function onNameSubmit({
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
onSubmit(false);
|
renameInput.value?.forceCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newName === props.name) {
|
if (newName === props.name) {
|
||||||
isNameEditEnabled.value = false;
|
renameInput.value?.forceCancel();
|
||||||
|
|
||||||
onSubmit(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,12 +392,11 @@ async function onNameSubmit({
|
|||||||
const id = getWorkflowId();
|
const id = getWorkflowId();
|
||||||
const saved = await workflowHelpers.saveCurrentWorkflow({ name });
|
const saved = await workflowHelpers.saveCurrentWorkflow({ name });
|
||||||
if (saved) {
|
if (saved) {
|
||||||
isNameEditEnabled.value = false;
|
|
||||||
showCreateWorkflowSuccessToast(id);
|
showCreateWorkflowSuccessToast(id);
|
||||||
workflowHelpers.setDocumentTitle(newName, 'IDLE');
|
workflowHelpers.setDocumentTitle(newName, 'IDLE');
|
||||||
}
|
}
|
||||||
uiStore.removeActiveAction('workflowSaving');
|
uiStore.removeActiveAction('workflowSaving');
|
||||||
onSubmit(saved);
|
renameInput.value?.forceCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileImport(): Promise<void> {
|
async function handleFileImport(): Promise<void> {
|
||||||
@@ -704,7 +688,7 @@ const onBreadcrumbsItemSelected = (item: PathItem) => {
|
|||||||
class="name-container"
|
class="name-container"
|
||||||
data-test-id="canvas-breadcrumbs"
|
data-test-id="canvas-breadcrumbs"
|
||||||
>
|
>
|
||||||
<template #default="{ value }">
|
<template #default>
|
||||||
<FolderBreadcrumbs
|
<FolderBreadcrumbs
|
||||||
:current-folder="currentFolderForBreadcrumbs"
|
:current-folder="currentFolderForBreadcrumbs"
|
||||||
:current-folder-as-link="true"
|
:current-folder-as-link="true"
|
||||||
@@ -716,28 +700,22 @@ const onBreadcrumbsItemSelected = (item: PathItem) => {
|
|||||||
:class="$style['path-separator']"
|
:class="$style['path-separator']"
|
||||||
>/</span
|
>/</span
|
||||||
>
|
>
|
||||||
<ShortenName :name="name" :limit="value" :custom="true" test-id="workflow-name-input">
|
<N8nInlineTextEdit
|
||||||
<template #default="{ shortenedName }">
|
ref="renameInput"
|
||||||
<InlineTextEdit
|
:key="id"
|
||||||
:model-value="name"
|
placeholder="Workflow name"
|
||||||
:preview-value="shortenedName"
|
data-test-id="workflow-name-input"
|
||||||
:is-edit-enabled="isNameEditEnabled"
|
class="name"
|
||||||
:max-length="MAX_WORKFLOW_NAME_LENGTH"
|
:model-value="name"
|
||||||
:disabled="
|
:max-length="MAX_WORKFLOW_NAME_LENGTH"
|
||||||
readOnly || isArchived || (!isNewWorkflow && !workflowPermissions.update)
|
:read-only="readOnly || isArchived || (!isNewWorkflow && !workflowPermissions.update)"
|
||||||
"
|
:disabled="readOnly || isArchived || (!isNewWorkflow && !workflowPermissions.update)"
|
||||||
placeholder="Enter workflow name"
|
@update:model-value="onNameSubmit"
|
||||||
class="name"
|
/>
|
||||||
@toggle="onNameToggle"
|
|
||||||
@submit="onNameSubmit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ShortenName>
|
|
||||||
</template>
|
</template>
|
||||||
</FolderBreadcrumbs>
|
</FolderBreadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
</BreakpointsObserver>
|
</BreakpointsObserver>
|
||||||
|
|
||||||
<span class="tags" data-test-id="workflow-tags-container">
|
<span class="tags" data-test-id="workflow-tags-container">
|
||||||
<template v-if="settingsStore.areTagsEnabled">
|
<template v-if="settingsStore.areTagsEnabled">
|
||||||
<WorkflowTagsDropdown
|
<WorkflowTagsDropdown
|
||||||
@@ -894,6 +872,7 @@ $--header-spacing: 20px;
|
|||||||
.name {
|
.name {
|
||||||
color: $custom-font-dark;
|
color: $custom-font-dark;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activator {
|
.activator {
|
||||||
@@ -971,7 +950,7 @@ $--header-spacing: 20px;
|
|||||||
.path-separator {
|
.path-separator {
|
||||||
font-size: var(--font-size-xl);
|
font-size: var(--font-size-xl);
|
||||||
color: var(--color-foreground-base);
|
color: var(--color-foreground-base);
|
||||||
margin: var(--spacing-4xs);
|
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
|
||||||
|
|
||||||
import NodeTitle from '@/components/NodeTitle.vue';
|
import NodeTitle from '@/components/NodeTitle.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
@@ -24,16 +23,17 @@ describe('NodeTitle', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(getByTestId('node-title-container')).toBeInTheDocument();
|
expect(getByTestId('node-title-container')).toBeInTheDocument();
|
||||||
expect(getByTestId('node-rename-input')).toBeInTheDocument();
|
expect(getByTestId('inline-edit-input')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the node title', () => {
|
it('displays the node title', () => {
|
||||||
const { getByText } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: 'My Test Node',
|
modelValue: 'Test Node',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(getByText('My Test Node')).toBeInTheDocument();
|
const renamePreview = getByTestId('inline-edit-preview');
|
||||||
|
expect(renamePreview).toHaveTextContent('Test Node');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the edit input when clicked', async () => {
|
it('shows the edit input when clicked', async () => {
|
||||||
@@ -43,53 +43,37 @@ describe('NodeTitle', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await userEvent.click(getByTestId('node-title-container'));
|
await userEvent.click(getByTestId('node-title-container'));
|
||||||
expect(getByTestId('node-rename-input')).toHaveValue('Test Node');
|
expect(getByTestId('inline-edit-input')).toHaveValue('Test Node');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits update:model-value when renaming', async () => {
|
it('emits update:model-value when renaming', async () => {
|
||||||
const { getByTestId, getByRole, emitted } = renderComponent({
|
const { getByTestId, emitted } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: 'Test Node',
|
modelValue: 'Test Node',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await userEvent.click(getByTestId('node-title-container'));
|
const renameInput = getByTestId('inline-edit-input');
|
||||||
const renameInput = getByTestId('node-rename-input');
|
|
||||||
await userEvent.clear(renameInput);
|
await userEvent.clear(renameInput);
|
||||||
await userEvent.type(renameInput, 'New Node Name');
|
await userEvent.type(renameInput, 'New Node Name');
|
||||||
|
|
||||||
expect(renameInput).toHaveValue('New Node Name');
|
expect(renameInput).toHaveValue('New Node Name');
|
||||||
|
|
||||||
await userEvent.click(getByRole('button', { name: 'Rename' }));
|
await userEvent.keyboard('{enter}');
|
||||||
|
|
||||||
expect(emitted('update:model-value')).toEqual([['New Node Name']]);
|
expect(emitted('update:model-value')).toEqual([['New Node Name']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancels renaming when cancel button is clicked', async () => {
|
it('should not update if user cancels', async () => {
|
||||||
const { getByTestId, getByRole, emitted } = renderComponent({
|
const { getByTestId, emitted } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: 'Test Node',
|
modelValue: 'Test Node',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await userEvent.click(getByTestId('node-title-container'));
|
const renameInput = getByTestId('inline-edit-input');
|
||||||
await userEvent.click(getByRole('button', { name: 'Cancel' }));
|
|
||||||
expect(emitted('update:model-value')).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call onRename when Enter is pressed on cancel button', async () => {
|
|
||||||
const { getByTestId, getByRole, emitted } = renderComponent({
|
|
||||||
props: {
|
|
||||||
modelValue: 'Test Node',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await userEvent.click(getByTestId('node-title-container'));
|
|
||||||
const renameInput = getByTestId('node-rename-input');
|
|
||||||
await userEvent.clear(renameInput);
|
|
||||||
await userEvent.type(renameInput, 'New Node Name');
|
await userEvent.type(renameInput, 'New Node Name');
|
||||||
|
await userEvent.keyboard('{Escape}');
|
||||||
expect(renameInput).toHaveValue('New Node Name');
|
await userEvent.click(renameInput);
|
||||||
|
expect(renameInput).toHaveValue('Test Node');
|
||||||
const cancelButton = getByRole('button', { name: 'Cancel' });
|
|
||||||
await fireEvent.focus(cancelButton);
|
|
||||||
await fireEvent.keyDown(cancelButton, { key: 'Enter' });
|
|
||||||
expect(emitted('update:model-value')).toBeUndefined();
|
expect(emitted('update:model-value')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { N8nInlineTextEdit } from '@n8n/design-system';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { computed, nextTick, ref } from 'vue';
|
import { useTemplateRef } from 'vue';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
@@ -10,81 +11,39 @@ type Props = {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
nodeType: undefined,
|
nodeType: undefined,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:model-value': [value: string];
|
'update:model-value': [value: string];
|
||||||
}>();
|
}>();
|
||||||
const editName = ref(false);
|
|
||||||
const newName = ref('');
|
|
||||||
const input = ref<HTMLInputElement>();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
function onRename(value: string) {
|
||||||
|
if (value.trim() !== '') {
|
||||||
const editable = computed(() => !props.readOnly && window === window.parent);
|
emit('update:model-value', value.trim());
|
||||||
|
|
||||||
async function onEdit() {
|
|
||||||
newName.value = props.modelValue;
|
|
||||||
editName.value = true;
|
|
||||||
await nextTick();
|
|
||||||
if (input.value) {
|
|
||||||
input.value.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRename() {
|
const wrapperRef = useTemplateRef('wrapperRef');
|
||||||
if (newName.value.trim() !== '') {
|
const { width } = useElementSize(wrapperRef);
|
||||||
emit('update:model-value', newName.value.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
editName.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span :class="$style.container" data-test-id="node-title-container" @click="onEdit">
|
<span :class="$style.container" data-test-id="node-title-container">
|
||||||
<span :class="$style.iconWrapper">
|
<span :class="$style.iconWrapper">
|
||||||
<NodeIcon :node-type="nodeType" :size="18" />
|
<NodeIcon :node-type="nodeType" :size="18" />
|
||||||
</span>
|
</span>
|
||||||
<n8n-popover placement="right" width="200" :visible="editName" :disabled="!editable">
|
<div ref="wrapperRef" :class="$style.textWrapper">
|
||||||
<div
|
<N8nInlineTextEdit
|
||||||
:class="$style.editContainer"
|
:max-width="width"
|
||||||
@keydown.enter="onRename"
|
:model-value="modelValue"
|
||||||
@keydown.stop
|
:read-only="readOnly"
|
||||||
@keydown.esc="editName = false"
|
@update:model-value="onRename"
|
||||||
>
|
/>
|
||||||
<n8n-text :bold="true" color="text-base" tag="div">{{
|
</div>
|
||||||
i18n.baseText('ndv.title.renameNode')
|
|
||||||
}}</n8n-text>
|
|
||||||
<n8n-input ref="input" v-model="newName" size="small" data-test-id="node-rename-input" />
|
|
||||||
<div :class="$style.editButtons">
|
|
||||||
<n8n-button
|
|
||||||
type="secondary"
|
|
||||||
size="small"
|
|
||||||
:label="i18n.baseText('ndv.title.cancel')"
|
|
||||||
@click="editName = false"
|
|
||||||
@keydown.enter.stop
|
|
||||||
/>
|
|
||||||
<n8n-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:label="i18n.baseText('ndv.title.rename')"
|
|
||||||
@click="onRename"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #reference>
|
|
||||||
<div :class="{ [$style.title]: true, [$style.hoverable]: editable }">
|
|
||||||
{{ modelValue }}
|
|
||||||
<div :class="$style.editIconContainer">
|
|
||||||
<font-awesome-icon v-if="editable" :class="$style.editIcon" icon="pencil-alt" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n8n-popover>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,62 +52,18 @@ function onRename() {
|
|||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-m);
|
||||||
line-height: var(--font-line-height-compact);
|
margin-right: var(--spacing-s);
|
||||||
overflow-wrap: anywhere;
|
|
||||||
padding-right: var(--spacing-s);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
max-height: 100px;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 5;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoverable {
|
.textWrapper {
|
||||||
&:hover {
|
display: flex;
|
||||||
cursor: pointer;
|
flex-grow: 1;
|
||||||
.editIcon {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconWrapper {
|
.iconWrapper {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-right: var(--spacing-2xs);
|
margin-right: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editIcon {
|
|
||||||
display: none;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--color-text-base);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editIconContainer {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editButtons {
|
|
||||||
text-align: right;
|
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin-left: var(--spacing-4xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editContainer {
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
> *:first-child {
|
|
||||||
margin-bottom: var(--spacing-4xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import unset from 'lodash/unset';
|
import unset from 'lodash/unset';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
NodeParameterValue,
|
NodeParameterValue,
|
||||||
@@ -38,7 +39,6 @@ import { useUIStore } from '@/stores/ui.store';
|
|||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
import { destinationToFakeINodeUi } from '@/components/SettingsLogStreaming/Helpers.ee';
|
import { destinationToFakeINodeUi } from '@/components/SettingsLogStreaming/Helpers.ee';
|
||||||
import type { BaseTextKey } from '@n8n/i18n';
|
import type { BaseTextKey } from '@n8n/i18n';
|
||||||
import InlineNameEdit from '@/components/InlineNameEdit.vue';
|
|
||||||
import SaveButton from '@/components/SaveButton.vue';
|
import SaveButton from '@/components/SaveButton.vue';
|
||||||
import EventSelection from '@/components/SettingsLogStreaming/EventSelection.ee.vue';
|
import EventSelection from '@/components/SettingsLogStreaming/EventSelection.ee.vue';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
@@ -49,6 +49,8 @@ import {
|
|||||||
sentryModalDescription,
|
sentryModalDescription,
|
||||||
syslogModalDescription,
|
syslogModalDescription,
|
||||||
} from './descriptions.ee';
|
} from './descriptions.ee';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { N8nInlineTextEdit, N8nText } from '@n8n/design-system';
|
||||||
|
|
||||||
defineOptions({ name: 'EventDestinationSettingsModal' });
|
defineOptions({ name: 'EventDestinationSettingsModal' });
|
||||||
|
|
||||||
@@ -353,6 +355,9 @@ function callEventBus(event: string, data: unknown) {
|
|||||||
eventBus.emit(event, data);
|
eventBus.emit(event, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defNameRef = useTemplateRef('defNameRef');
|
||||||
|
const { width } = useElementSize(defNameRef);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -377,15 +382,17 @@ function callEventBus(event: string, data: unknown) {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<div :class="$style.destinationInfo">
|
<div ref="defNameRef" :class="$style.destinationInfo">
|
||||||
<InlineNameEdit
|
<N8nInlineTextEdit
|
||||||
:model-value="headerLabel"
|
:max-width="width - 10"
|
||||||
:subtitle="!isTypeAbstract ? i18n.baseText(typeLabelName) : 'Select type'"
|
|
||||||
:readonly="isTypeAbstract"
|
|
||||||
type="Credential"
|
|
||||||
data-test-id="subtitle-showing-type"
|
data-test-id="subtitle-showing-type"
|
||||||
|
:model-value="headerLabel"
|
||||||
|
:readonly="isTypeAbstract"
|
||||||
@update:model-value="onLabelChange"
|
@update:model-value="onLabelChange"
|
||||||
/>
|
/>
|
||||||
|
<N8nText size="small" tag="p" color="text-light">{{
|
||||||
|
!isTypeAbstract ? i18n.baseText(typeLabelName) : 'Select type'
|
||||||
|
}}</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.destinationActions">
|
<div :class="$style.destinationActions">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
@@ -587,10 +594,11 @@ function callEventBus(event: string, data: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.destinationInfo {
|
.destinationInfo {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -857,6 +857,13 @@ function onRenameNode(parameterData: IUpdateInformation) {
|
|||||||
async function onOpenRenameNodeModal(id: string) {
|
async function onOpenRenameNodeModal(id: string) {
|
||||||
const currentName = workflowsStore.getNodeById(id)?.name ?? '';
|
const currentName = workflowsStore.getNodeById(id)?.name ?? '';
|
||||||
|
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
|
||||||
|
if (activeElement && activeElement.tagName === 'INPUT') {
|
||||||
|
// If an input is focused, do not open the rename modal
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!keyBindingsEnabled.value || document.querySelector('.rename-prompt')) return;
|
if (!keyBindingsEnabled.value || document.querySelector('.rename-prompt')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
N8nCard,
|
N8nCard,
|
||||||
N8nHeading,
|
N8nHeading,
|
||||||
N8nIcon,
|
N8nIcon,
|
||||||
|
N8nInlineTextEdit,
|
||||||
N8nInputLabel,
|
N8nInputLabel,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
@@ -64,7 +65,7 @@ import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Brea
|
|||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { PROJECT_ROOT } from 'n8n-workflow';
|
import { PROJECT_ROOT } from 'n8n-workflow';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE_TIME = 300;
|
const SEARCH_DEBOUNCE_TIME = 300;
|
||||||
@@ -140,8 +141,6 @@ const currentFolderId = ref<string | null>(null);
|
|||||||
|
|
||||||
const showCardsBadge = ref(false);
|
const showCardsBadge = ref(false);
|
||||||
|
|
||||||
const isNameEditEnabled = ref(false);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder actions
|
* Folder actions
|
||||||
* These can appear on the list header, and then they are applied to current folder
|
* These can appear on the list header, and then they are applied to current folder
|
||||||
@@ -1413,17 +1412,16 @@ const onCreateWorkflowClick = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNameToggle = () => {
|
const renameInput = useTemplateRef('renameInput');
|
||||||
isNameEditEnabled.value = !isNameEditEnabled.value;
|
function onNameToggle() {
|
||||||
};
|
setTimeout(() => {
|
||||||
|
if (renameInput.value?.forceFocus) {
|
||||||
|
renameInput.value.forceFocus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
const onNameSubmit = async ({
|
const onNameSubmit = async (name: string) => {
|
||||||
name,
|
|
||||||
onSubmit,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
onSubmit: (saved: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
if (!currentFolder.value || !currentProject.value) return;
|
if (!currentFolder.value || !currentProject.value) return;
|
||||||
|
|
||||||
const newName = name.trim();
|
const newName = name.trim();
|
||||||
@@ -1434,14 +1432,11 @@ const onNameSubmit = async ({
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
onSubmit(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newName === currentFolder.value.name) {
|
if (newName === currentFolder.value.name) {
|
||||||
isNameEditEnabled.value = false;
|
renameInput.value?.forceCancel();
|
||||||
|
|
||||||
onSubmit(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,7 +1447,7 @@ const onNameSubmit = async ({
|
|||||||
message: validationResult,
|
message: validationResult,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
onSubmit(false);
|
renameInput.value?.forceCancel();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -1467,11 +1462,9 @@ const onNameSubmit = async ({
|
|||||||
telemetry.track('User renamed folder', {
|
telemetry.track('User renamed folder', {
|
||||||
folder_id: currentFolder.value.id,
|
folder_id: currentFolder.value.id,
|
||||||
});
|
});
|
||||||
isNameEditEnabled.value = false;
|
|
||||||
onSubmit(true);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('folders.rename.error.title'));
|
toast.showError(error, i18n.baseText('folders.rename.error.title'));
|
||||||
onSubmit(false);
|
renameInput.value?.forceCancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1605,17 +1598,16 @@ const onNameSubmit = async ({
|
|||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<span :class="$style['path-separator']">/</span>
|
<span :class="$style['path-separator']">/</span>
|
||||||
<InlineTextEdit
|
<N8nInlineTextEdit
|
||||||
|
ref="renameInput"
|
||||||
|
:key="currentFolder?.id"
|
||||||
data-test-id="breadcrumbs-item-current"
|
data-test-id="breadcrumbs-item-current"
|
||||||
:model-value="currentFolder.name"
|
|
||||||
:preview-value="currentFolder.name"
|
|
||||||
:is-edit-enabled="isNameEditEnabled"
|
|
||||||
:max-length="30"
|
|
||||||
:disabled="readOnlyEnv || !hasPermissionToUpdateFolders"
|
|
||||||
:class="{ [$style.name]: true, [$style['pointer-disabled']]: isDragging }"
|
|
||||||
:placeholder="i18n.baseText('folders.rename.placeholder')"
|
:placeholder="i18n.baseText('folders.rename.placeholder')"
|
||||||
@toggle="onNameToggle"
|
:model-value="currentFolder.name"
|
||||||
@submit="onNameSubmit"
|
:max-length="30"
|
||||||
|
:read-only="readOnlyEnv || !hasPermissionToUpdateFolders"
|
||||||
|
:class="{ [$style.name]: true, [$style['pointer-disabled']]: isDragging }"
|
||||||
|
@update:model-value="onNameSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FolderBreadcrumbs>
|
</FolderBreadcrumbs>
|
||||||
@@ -1938,12 +1930,13 @@ const onNameSubmit = async ({
|
|||||||
.path-separator {
|
.path-separator {
|
||||||
font-size: var(--font-size-xl);
|
font-size: var(--font-size-xl);
|
||||||
color: var(--color-foreground-base);
|
color: var(--color-foreground-base);
|
||||||
margin: var(--spacing-4xs);
|
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
color: $custom-font-dark;
|
color: $custom-font-dark;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
padding: var(--spacing-3xs) var(--spacing-4xs) var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer-disabled {
|
.pointer-disabled {
|
||||||
|
|||||||
142
pnpm-lock.yaml
generated
142
pnpm-lock.yaml
generated
@@ -1789,6 +1789,9 @@ importers:
|
|||||||
parse-diff:
|
parse-diff:
|
||||||
specifier: ^0.11.1
|
specifier: ^0.11.1
|
||||||
version: 0.11.1
|
version: 0.11.1
|
||||||
|
reka-ui:
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||||
sanitize-html:
|
sanitize-html:
|
||||||
specifier: 2.12.1
|
specifier: 2.12.1
|
||||||
version: 2.12.1
|
version: 2.12.1
|
||||||
@@ -4006,9 +4009,21 @@ packages:
|
|||||||
'@floating-ui/core@1.3.1':
|
'@floating-ui/core@1.3.1':
|
||||||
resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==}
|
resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.0':
|
||||||
|
resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==}
|
||||||
|
|
||||||
'@floating-ui/dom@1.4.5':
|
'@floating-ui/dom@1.4.5':
|
||||||
resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==}
|
resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.0':
|
||||||
|
resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==}
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.9':
|
||||||
|
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.6':
|
||||||
|
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@0.2.36':
|
'@fortawesome/fontawesome-common-types@0.2.36':
|
||||||
resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==}
|
resolution: {integrity: sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4245,6 +4260,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@internationalized/date@3.8.1':
|
||||||
|
resolution: {integrity: sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA==}
|
||||||
|
|
||||||
|
'@internationalized/number@3.6.2':
|
||||||
|
resolution: {integrity: sha512-E5QTOlMg9wo5OrKdHD6edo1JJlIoOsylh0+mbf0evi1tHJwMZfJSaBpGtnJV9N7w3jeiioox9EG/EWRWPh82vg==}
|
||||||
|
|
||||||
'@intlify/core-base@11.1.2':
|
'@intlify/core-base@11.1.2':
|
||||||
resolution: {integrity: sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==}
|
resolution: {integrity: sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
@@ -6195,6 +6216,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-gB3NukbIcYzRtPoE6dx9svQYPodxvnfQlaaQd8N/z87E6WaMfRE7o5HwB+LZ+KeM0nsNAq1n4TmBtfz1VCUR+Q==}
|
resolution: {integrity: sha512-gB3NukbIcYzRtPoE6dx9svQYPodxvnfQlaaQd8N/z87E6WaMfRE7o5HwB+LZ+KeM0nsNAq1n4TmBtfz1VCUR+Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
'@sxzz/popperjs-es@2.11.7':
|
'@sxzz/popperjs-es@2.11.7':
|
||||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||||
|
|
||||||
@@ -6202,12 +6226,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
|
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.9':
|
||||||
|
resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
|
||||||
|
|
||||||
'@tanstack/vue-table@8.21.2':
|
'@tanstack/vue-table@8.21.2':
|
||||||
resolution: {integrity: sha512-KBgOWxha/x4m1EdhVWxOpqHb661UjqAxzPcmXR3QiA7aShZ547x19Gw0UJX9we+m+tVcPuLRZ61JsYW47QZFfQ==}
|
resolution: {integrity: sha512-KBgOWxha/x4m1EdhVWxOpqHb661UjqAxzPcmXR3QiA7aShZ547x19Gw0UJX9we+m+tVcPuLRZ61JsYW47QZFfQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>=3.2'
|
vue: '>=3.2'
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.9':
|
||||||
|
resolution: {integrity: sha512-HsvHaOo+o52cVcPhomKDZ3CMpTF/B2qg+BhPHIQJwzn4VIqDyt/rRVqtIomG6jE83IFsE2vlr6cmx7h3dHA0SA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^2.7.0 || ^3.0.0
|
||||||
|
|
||||||
'@techteamer/ocsp@1.0.1':
|
'@techteamer/ocsp@1.0.1':
|
||||||
resolution: {integrity: sha512-q4pW5wAC6Pc3JI8UePwE37CkLQ5gDGZMgjSX4MEEm4D4Di59auDQ8UNIDzC4gRnPNmmcwjpPxozq8p5pjiOmOw==}
|
resolution: {integrity: sha512-q4pW5wAC6Pc3JI8UePwE37CkLQ5gDGZMgjSX4MEEm4D4Di59auDQ8UNIDzC4gRnPNmmcwjpPxozq8p5pjiOmOw==}
|
||||||
|
|
||||||
@@ -6689,6 +6721,9 @@ packages:
|
|||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@types/webidl-conversions@7.0.0':
|
'@types/webidl-conversions@7.0.0':
|
||||||
resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==}
|
resolution: {integrity: sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==}
|
||||||
|
|
||||||
@@ -7008,18 +7043,27 @@ packages:
|
|||||||
'@vueuse/core@10.11.0':
|
'@vueuse/core@10.11.0':
|
||||||
resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==}
|
resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==}
|
||||||
|
|
||||||
|
'@vueuse/core@12.8.2':
|
||||||
|
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||||
|
|
||||||
'@vueuse/core@9.13.0':
|
'@vueuse/core@9.13.0':
|
||||||
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
|
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
|
||||||
|
|
||||||
'@vueuse/metadata@10.11.0':
|
'@vueuse/metadata@10.11.0':
|
||||||
resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==}
|
resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==}
|
||||||
|
|
||||||
|
'@vueuse/metadata@12.8.2':
|
||||||
|
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||||
|
|
||||||
'@vueuse/metadata@9.13.0':
|
'@vueuse/metadata@9.13.0':
|
||||||
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
|
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.0':
|
'@vueuse/shared@10.11.0':
|
||||||
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
|
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
|
||||||
|
|
||||||
|
'@vueuse/shared@12.8.2':
|
||||||
|
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||||
|
|
||||||
'@vueuse/shared@9.13.0':
|
'@vueuse/shared@9.13.0':
|
||||||
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
|
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
|
||||||
|
|
||||||
@@ -7239,6 +7283,10 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
aria-hidden@1.2.6:
|
||||||
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
aria-query@5.1.3:
|
aria-query@5.1.3:
|
||||||
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
|
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
|
||||||
|
|
||||||
@@ -8368,6 +8416,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
defu@6.1.4:
|
||||||
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -11483,6 +11534,9 @@ packages:
|
|||||||
obuf@1.1.2:
|
obuf@1.1.2:
|
||||||
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||||
|
|
||||||
|
ohash@2.0.11:
|
||||||
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
ollama@0.5.9:
|
ollama@0.5.9:
|
||||||
resolution: {integrity: sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==}
|
resolution: {integrity: sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==}
|
||||||
|
|
||||||
@@ -12381,6 +12435,11 @@ packages:
|
|||||||
reinterval@1.1.0:
|
reinterval@1.1.0:
|
||||||
resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==}
|
resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==}
|
||||||
|
|
||||||
|
reka-ui@2.2.1:
|
||||||
|
resolution: {integrity: sha512-oLHiyBn6gTIQGnTnv8G5LQuFp9j8HuUNl0qdnW3XPhFb/07hrxzFpjo2kt/jxOZive+n/XWDbOjSj2h9Hih3qA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>= 3.2.0'
|
||||||
|
|
||||||
relateurl@0.2.7:
|
relateurl@0.2.7:
|
||||||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -16579,10 +16638,30 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/core@1.3.1': {}
|
'@floating-ui/core@1.3.1': {}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.0':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
|
||||||
'@floating-ui/dom@1.4.5':
|
'@floating-ui/dom@1.4.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.3.1
|
'@floating-ui/core': 1.3.1
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.0':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.7.0
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.9': {}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.6(vue@3.5.13(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.0
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.2))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@0.2.36': {}
|
'@fortawesome/fontawesome-common-types@0.2.36': {}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@6.2.0': {}
|
'@fortawesome/fontawesome-common-types@6.2.0': {}
|
||||||
@@ -16826,6 +16905,14 @@ snapshots:
|
|||||||
'@img/sharp-win32-x64@0.33.5':
|
'@img/sharp-win32-x64@0.33.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@internationalized/date@3.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
|
'@internationalized/number@3.6.2':
|
||||||
|
dependencies:
|
||||||
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
'@intlify/core-base@11.1.2':
|
'@intlify/core-base@11.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/message-compiler': 11.1.2
|
'@intlify/message-compiler': 11.1.2
|
||||||
@@ -19159,15 +19246,26 @@ snapshots:
|
|||||||
|
|
||||||
'@supercharge/promise-pool@3.1.0': {}
|
'@supercharge/promise-pool@3.1.0': {}
|
||||||
|
|
||||||
|
'@swc/helpers@0.5.17':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@sxzz/popperjs-es@2.11.7': {}
|
'@sxzz/popperjs-es@2.11.7': {}
|
||||||
|
|
||||||
'@tanstack/table-core@8.21.2': {}
|
'@tanstack/table-core@8.21.2': {}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.9': {}
|
||||||
|
|
||||||
'@tanstack/vue-table@8.21.2(vue@3.5.13(typescript@5.8.2))':
|
'@tanstack/vue-table@8.21.2(vue@3.5.13(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/table-core': 8.21.2
|
'@tanstack/table-core': 8.21.2
|
||||||
vue: 3.5.13(typescript@5.8.2)
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
|
||||||
|
'@tanstack/vue-virtual@3.13.9(vue@3.5.13(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.13.9
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
|
||||||
'@techteamer/ocsp@1.0.1':
|
'@techteamer/ocsp@1.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
asn1.js: 5.4.1
|
asn1.js: 5.4.1
|
||||||
@@ -19717,6 +19815,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@types/webidl-conversions@7.0.0': {}
|
'@types/webidl-conversions@7.0.0': {}
|
||||||
|
|
||||||
'@types/whatwg-url@11.0.4':
|
'@types/whatwg-url@11.0.4':
|
||||||
@@ -20185,6 +20285,15 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/core@12.8.2(typescript@5.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 12.8.2
|
||||||
|
'@vueuse/shared': 12.8.2(typescript@5.8.2)
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@vueuse/core@9.13.0(vue@3.5.13(typescript@5.8.2))':
|
'@vueuse/core@9.13.0(vue@3.5.13(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/web-bluetooth': 0.0.16
|
'@types/web-bluetooth': 0.0.16
|
||||||
@@ -20197,6 +20306,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vueuse/metadata@10.11.0': {}
|
'@vueuse/metadata@10.11.0': {}
|
||||||
|
|
||||||
|
'@vueuse/metadata@12.8.2': {}
|
||||||
|
|
||||||
'@vueuse/metadata@9.13.0': {}
|
'@vueuse/metadata@9.13.0': {}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.0(vue@3.5.13(typescript@5.8.2))':
|
'@vueuse/shared@10.11.0(vue@3.5.13(typescript@5.8.2))':
|
||||||
@@ -20206,6 +20317,12 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@vueuse/shared@12.8.2(typescript@5.8.2)':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@vueuse/shared@9.13.0(vue@3.5.13(typescript@5.8.2))':
|
'@vueuse/shared@9.13.0(vue@3.5.13(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.2))
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.2))
|
||||||
@@ -20431,6 +20548,10 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
aria-hidden@1.2.6:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
aria-query@5.1.3:
|
aria-query@5.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-equal: 2.2.0
|
deep-equal: 2.2.0
|
||||||
@@ -21755,6 +21876,8 @@ snapshots:
|
|||||||
has-property-descriptors: 1.0.2
|
has-property-descriptors: 1.0.2
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
|
|
||||||
|
defu@6.1.4: {}
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
delegates@1.0.0:
|
delegates@1.0.0:
|
||||||
@@ -25787,6 +25910,8 @@ snapshots:
|
|||||||
|
|
||||||
obuf@1.1.2: {}
|
obuf@1.1.2: {}
|
||||||
|
|
||||||
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
ollama@0.5.9:
|
ollama@0.5.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-fetch: 3.6.20
|
whatwg-fetch: 3.6.20
|
||||||
@@ -26774,6 +26899,23 @@ snapshots:
|
|||||||
|
|
||||||
reinterval@1.1.0: {}
|
reinterval@1.1.0: {}
|
||||||
|
|
||||||
|
reka-ui@2.2.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)):
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.0
|
||||||
|
'@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.8.2))
|
||||||
|
'@internationalized/date': 3.8.1
|
||||||
|
'@internationalized/number': 3.6.2
|
||||||
|
'@tanstack/vue-virtual': 3.13.9(vue@3.5.13(typescript@5.8.2))
|
||||||
|
'@vueuse/core': 12.8.2(typescript@5.8.2)
|
||||||
|
'@vueuse/shared': 12.8.2(typescript@5.8.2)
|
||||||
|
aria-hidden: 1.2.6
|
||||||
|
defu: 6.1.4
|
||||||
|
ohash: 2.0.11
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- typescript
|
||||||
|
|
||||||
relateurl@0.2.7: {}
|
relateurl@0.2.7: {}
|
||||||
|
|
||||||
remove-trailing-slash@0.1.1: {}
|
remove-trailing-slash@0.1.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user