refactor(editor): Standardize components sections order (no-changelog) (#10540)

This commit is contained in:
Ricardo Espinoza
2024-08-24 09:24:08 -04:00
committed by GitHub
parent 4d48f903af
commit 609bc4d97d
215 changed files with 10387 additions and 10376 deletions

View File

@@ -66,6 +66,12 @@ module.exports = {
useAttrs: 'attrs',
},
],
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
// TODO: fix these
'@typescript-eslint/no-unsafe-call': 'off',

View File

@@ -1,3 +1,29 @@
<script lang="ts" setup>
import N8nButton from '../N8nButton';
import N8nHeading from '../N8nHeading';
import N8nText from '../N8nText';
import N8nCallout, { type CalloutTheme } from '../N8nCallout';
import type { ButtonType } from 'n8n-design-system/types/button';
import N8nTooltip from 'n8n-design-system/components/N8nTooltip/Tooltip.vue';
interface ActionBoxProps {
emoji: string;
heading: string;
buttonText: string;
buttonType: ButtonType;
buttonDisabled?: boolean;
description: string;
calloutText?: string;
calloutTheme?: CalloutTheme;
calloutIcon?: string;
}
defineOptions({ name: 'N8nActionBox' });
withDefaults(defineProps<ActionBoxProps>(), {
calloutTheme: 'info',
});
</script>
<template>
<div :class="['n8n-action-box', $style.container]" data-test-id="action-box">
<div v-if="emoji" :class="$style.emoji">
@@ -41,32 +67,6 @@
</div>
</template>
<script lang="ts" setup>
import N8nButton from '../N8nButton';
import N8nHeading from '../N8nHeading';
import N8nText from '../N8nText';
import N8nCallout, { type CalloutTheme } from '../N8nCallout';
import type { ButtonType } from 'n8n-design-system/types/button';
import N8nTooltip from 'n8n-design-system/components/N8nTooltip/Tooltip.vue';
interface ActionBoxProps {
emoji: string;
heading: string;
buttonText: string;
buttonType: ButtonType;
buttonDisabled?: boolean;
description: string;
calloutText?: string;
calloutTheme?: CalloutTheme;
calloutIcon?: string;
}
defineOptions({ name: 'N8nActionBox' });
withDefaults(defineProps<ActionBoxProps>(), {
calloutTheme: 'info',
});
</script>
<style lang="scss" module>
.container {
border: 2px dashed var(--color-foreground-base);

View File

@@ -1,57 +1,3 @@
<template>
<div :class="['action-dropdown-container', $style.actionDropdownContainer]">
<ElDropdown
ref="elementDropdown"
:placement="placement"
:trigger="trigger"
:popper-class="popperClass"
:teleported="teleported"
:disabled="disabled"
@command="onSelect"
@visible-change="onVisibleChange"
>
<slot v-if="$slots.activator" name="activator" />
<n8n-icon-button
v-else
type="tertiary"
text
:class="$style.activator"
:size="activatorSize"
:icon="activatorIcon"
@blur="onButtonBlur"
/>
<template #dropdown>
<ElDropdownMenu :class="$style.userActionsMenu">
<ElDropdownItem
v-for="item in items"
:key="item.id"
:command="item.id"
:disabled="item.disabled"
:divided="item.divided"
:class="$style.elementItem"
>
<div :class="getItemClasses(item)" :data-test-id="`${testIdPrefix}-item-${item.id}`">
<span v-if="item.icon" :class="$style.icon">
<N8nIcon :icon="item.icon" :size="iconSize" />
</span>
<span :class="$style.label">
{{ item.label }}
</span>
<N8nKeyboardShortcut
v-if="item.shortcut"
v-bind="item.shortcut"
:class="$style.shortcut"
>
</N8nKeyboardShortcut>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
<script lang="ts" setup>
// This component is visually similar to the ActionToggle component
// but it offers more options when it comes to dropdown items styling
@@ -129,6 +75,60 @@ const close = () => elementDropdown.value?.handleClose();
defineExpose({ open, close });
</script>
<template>
<div :class="['action-dropdown-container', $style.actionDropdownContainer]">
<ElDropdown
ref="elementDropdown"
:placement="placement"
:trigger="trigger"
:popper-class="popperClass"
:teleported="teleported"
:disabled="disabled"
@command="onSelect"
@visible-change="onVisibleChange"
>
<slot v-if="$slots.activator" name="activator" />
<n8n-icon-button
v-else
type="tertiary"
text
:class="$style.activator"
:size="activatorSize"
:icon="activatorIcon"
@blur="onButtonBlur"
/>
<template #dropdown>
<ElDropdownMenu :class="$style.userActionsMenu">
<ElDropdownItem
v-for="item in items"
:key="item.id"
:command="item.id"
:disabled="item.disabled"
:divided="item.divided"
:class="$style.elementItem"
>
<div :class="getItemClasses(item)" :data-test-id="`${testIdPrefix}-item-${item.id}`">
<span v-if="item.icon" :class="$style.icon">
<N8nIcon :icon="item.icon" :size="iconSize" />
</span>
<span :class="$style.label">
{{ item.label }}
</span>
<N8nKeyboardShortcut
v-if="item.shortcut"
v-bind="item.shortcut"
:class="$style.shortcut"
>
</N8nKeyboardShortcut>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
<style lang="scss" module>
:global(.el-dropdown__list) {
.userActionsMenu {

View File

@@ -1,3 +1,38 @@
<script lang="ts" setup>
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import type { UserAction } from 'n8n-design-system/types';
import N8nIcon from '../N8nIcon';
import type { IconOrientation, IconSize } from 'n8n-design-system/types/icon';
const SIZE = ['mini', 'small', 'medium'] as const;
const THEME = ['default', 'dark'] as const;
interface ActionToggleProps {
actions?: UserAction[];
placement?: Placement;
size?: (typeof SIZE)[number];
iconSize?: IconSize;
theme?: (typeof THEME)[number];
iconOrientation?: IconOrientation;
}
defineOptions({ name: 'N8nActionToggle' });
withDefaults(defineProps<ActionToggleProps>(), {
actions: () => [],
placement: 'bottom',
size: 'medium',
theme: 'default',
iconOrientation: 'vertical',
});
const emit = defineEmits<{
action: [value: string];
'visible-change': [value: boolean];
}>();
const onCommand = (value: string) => emit('action', value);
const onVisibleChange = (value: boolean) => emit('visible-change', value);
</script>
<template>
<span :class="$style.container" data-test-id="action-toggle" @click.stop.prevent>
<ElDropdown
@@ -41,41 +76,6 @@
</span>
</template>
<script lang="ts" setup>
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import type { UserAction } from 'n8n-design-system/types';
import N8nIcon from '../N8nIcon';
import type { IconOrientation, IconSize } from 'n8n-design-system/types/icon';
const SIZE = ['mini', 'small', 'medium'] as const;
const THEME = ['default', 'dark'] as const;
interface ActionToggleProps {
actions?: UserAction[];
placement?: Placement;
size?: (typeof SIZE)[number];
iconSize?: IconSize;
theme?: (typeof THEME)[number];
iconOrientation?: IconOrientation;
}
defineOptions({ name: 'N8nActionToggle' });
withDefaults(defineProps<ActionToggleProps>(), {
actions: () => [],
placement: 'bottom',
size: 'medium',
theme: 'default',
iconOrientation: 'vertical',
});
const emit = defineEmits<{
action: [value: string];
'visible-change': [value: boolean];
}>();
const onCommand = (value: string) => emit('action', value);
const onVisibleChange = (value: boolean) => emit('visible-change', value);
</script>
<style lang="scss" module>
.container > * {
line-height: 1;

View File

@@ -1,28 +1,3 @@
<template>
<div :class="alertBoxClassNames" role="alert">
<div :class="$style.content">
<span v-if="showIcon || $slots.icon" :class="$style.icon">
<N8nIcon v-if="showIcon" :icon="icon" />
<slot v-else-if="$slots.icon" name="icon" />
</span>
<div :class="$style.text">
<div v-if="$slots.title || title" :class="$style.title">
<slot name="title">{{ title }}</slot>
</div>
<div
v-if="$slots.default || description"
:class="{ [$style.description]: true, [$style.hasTitle]: $slots.title || title }"
>
<slot>{{ description }}</slot>
</div>
</div>
</div>
<div v-if="$slots.aside" :class="$style.aside">
<slot name="aside" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nIcon from '../N8nIcon';
@@ -76,6 +51,31 @@ const alertBoxClassNames = computed(() => {
});
</script>
<template>
<div :class="alertBoxClassNames" role="alert">
<div :class="$style.content">
<span v-if="showIcon || $slots.icon" :class="$style.icon">
<N8nIcon v-if="showIcon" :icon="icon" />
<slot v-else-if="$slots.icon" name="icon" />
</span>
<div :class="$style.text">
<div v-if="$slots.title || title" :class="$style.title">
<slot name="title">{{ title }}</slot>
</div>
<div
v-if="$slots.default || description"
:class="{ [$style.description]: true, [$style.hasTitle]: $slots.title || title }"
>
<slot>{{ description }}</slot>
</div>
</div>
</div>
<div v-if="$slots.aside" :class="$style.aside">
<slot name="aside" />
</div>
</div>
</template>
<style lang="scss" module>
@import '../../css/common/var.scss';

View File

@@ -1,19 +1,3 @@
<template>
<span :class="['n8n-avatar', $style.container]" v-bind="$attrs">
<Avatar
v-if="name"
:size="getSize(size)"
:name="name"
variant="marble"
:colors="getColors(colors)"
/>
<div v-else :class="[$style.empty, $style[size]]"></div>
<span v-if="firstName || lastName" :class="[$style.initials, $style[`text-${size}`]]">
{{ initials }}
</span>
</span>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import Avatar from 'vue-boring-avatars';
@@ -57,6 +41,22 @@ const sizes: { [size: string]: number } = {
const getSize = (size: string): number => sizes[size];
</script>
<template>
<span :class="['n8n-avatar', $style.container]" v-bind="$attrs">
<Avatar
v-if="name"
:size="getSize(size)"
:name="name"
variant="marble"
:colors="getColors(colors)"
/>
<div v-else :class="[$style.empty, $style[size]]"></div>
<span v-if="firstName || lastName" :class="[$style.initials, $style[`text-${size}`]]">
{{ initials }}
</span>
</span>
</template>
<style lang="scss" module>
.container {
position: relative;

View File

@@ -1,11 +1,3 @@
<template>
<span :class="['n8n-badge', $style[theme]]">
<N8nText :size="size" :bold="bold" :compact="true">
<slot></slot>
</N8nText>
</span>
</template>
<script lang="ts" setup>
import type { TextSize } from 'n8n-design-system/types/text';
import N8nText from '../N8nText';
@@ -34,6 +26,14 @@ withDefaults(defineProps<BadgeProps>(), {
});
</script>
<template>
<span :class="['n8n-badge', $style[theme]]">
<N8nText :size="size" :bold="bold" :compact="true">
<slot></slot>
</N8nText>
</span>
</template>
<style lang="scss" module>
.badge {
display: inline-flex;

View File

@@ -1,3 +1,13 @@
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
};
withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<template>
<transition name="fade" mode="out-in">
<div
@@ -9,16 +19,6 @@
</transition>
</template>
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
};
withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<style lang="scss" module>
.uiBlocker {
position: absolute;

View File

@@ -1,27 +1,3 @@
<template>
<component
:is="element"
:class="classes"
:disabled="isDisabled"
:aria-disabled="ariaDisabled"
:aria-busy="ariaBusy"
:href="href"
aria-live="polite"
v-bind="{
...attrs,
...(props.nativeType ? { type: props.nativeType } : {}),
}"
>
<span v-if="loading || icon" :class="$style.icon">
<N8nSpinner v-if="loading" :size="iconSize" />
<N8nIcon v-else-if="icon" :icon="icon" :size="iconSize" />
</span>
<span v-if="label || $slots.default">
<slot>{{ label }}</slot>
</span>
</component>
</template>
<script setup lang="ts">
import { useCssModule, computed, useAttrs, watchEffect } from 'vue';
import N8nIcon from '../N8nIcon';
@@ -75,6 +51,30 @@ const classes = computed(() => {
});
</script>
<template>
<component
:is="element"
:class="classes"
:disabled="isDisabled"
:aria-disabled="ariaDisabled"
:aria-busy="ariaBusy"
:href="href"
aria-live="polite"
v-bind="{
...attrs,
...(props.nativeType ? { type: props.nativeType } : {}),
}"
>
<span v-if="loading || icon" :class="$style.icon">
<N8nSpinner v-if="loading" :size="iconSize" />
<N8nIcon v-else-if="icon" :icon="icon" :size="iconSize" />
</span>
<span v-if="label || $slots.default">
<slot>{{ label }}</slot>
</span>
</component>
</template>
<style lang="scss">
@import './Button';

View File

@@ -1,20 +1,3 @@
<template>
<div :class="classes" role="alert">
<div :class="$style.messageSection">
<div v-if="!iconless" :class="$style.icon">
<N8nIcon :icon="getIcon" :size="getIconSize" />
</div>
<N8nText size="small">
<slot />
</N8nText>
&nbsp;
<slot name="actions" />
</div>
<slot name="trailingContent" />
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText';
@@ -70,6 +53,23 @@ const getIconSize = computed<IconSize>(() => {
});
</script>
<template>
<div :class="classes" role="alert">
<div :class="$style.messageSection">
<div v-if="!iconless" :class="$style.icon">
<N8nIcon :icon="getIcon" :size="getIconSize" />
</div>
<N8nText size="small">
<slot />
</N8nText>
&nbsp;
<slot name="actions" />
</div>
<slot name="trailingContent" />
</div>
</template>
<style lang="scss" module>
.callout {
display: flex;

View File

@@ -1,3 +1,23 @@
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
interface CardProps {
hoverable?: boolean;
}
defineOptions({ name: 'N8nCard' });
const props = withDefaults(defineProps<CardProps>(), {
hoverable: false,
});
const $style = useCssModule();
const classes = computed(() => ({
card: true,
[$style.card]: true,
[$style.hoverable]: props.hoverable,
}));
</script>
<template>
<div :class="classes" v-bind="$attrs">
<div v-if="$slots.prepend" :class="$style.icon">
@@ -20,26 +40,6 @@
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
interface CardProps {
hoverable?: boolean;
}
defineOptions({ name: 'N8nCard' });
const props = withDefaults(defineProps<CardProps>(), {
hoverable: false,
});
const $style = useCssModule();
const classes = computed(() => ({
card: true,
[$style.card]: true,
[$style.hoverable]: props.hoverable,
}));
</script>
<style lang="scss" module>
.card {
border-radius: var(--border-radius-large);

View File

@@ -1,25 +1,3 @@
<template>
<ElCheckbox
v-bind="$props"
ref="checkbox"
:class="['n8n-checkbox', $style.n8nCheckbox]"
:disabled="disabled"
:indeterminate="indeterminate"
:model-value="modelValue"
@update:model-value="onUpdateModelValue"
>
<slot></slot>
<N8nInputLabel
v-if="label"
:label="label"
:tooltip-text="tooltipText"
:bold="false"
:size="labelSize"
@click.prevent="onLabelClick"
/>
</ElCheckbox>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ElCheckbox } from 'element-plus';
@@ -58,6 +36,28 @@ const onLabelClick = () => {
};
</script>
<template>
<ElCheckbox
v-bind="$props"
ref="checkbox"
:class="['n8n-checkbox', $style.n8nCheckbox]"
:disabled="disabled"
:indeterminate="indeterminate"
:model-value="modelValue"
@update:model-value="onUpdateModelValue"
>
<slot></slot>
<N8nInputLabel
v-if="label"
:label="label"
:tooltip-text="tooltipText"
:bold="false"
:size="labelSize"
@click.prevent="onLabelClick"
/>
</ElCheckbox>
</template>
<style lang="scss" module>
.n8nCheckbox {
display: flex !important;

View File

@@ -1,26 +1,3 @@
<template>
<div class="progress-circle">
<svg class="progress-ring" :width="diameter" :height="diameter">
<circle
:class="$style.progressRingCircle"
:stroke-width="strokeWidth"
stroke="#DCDFE6"
fill="transparent"
:r="radius"
v-bind="{ cx, cy }"
/>
<circle
:class="$style.progressRingCircle"
stroke="#5C4EC2"
:stroke-width="strokeWidth"
fill="transparent"
:r="radius"
v-bind="{ cx, cy, style }"
/>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(
@@ -50,6 +27,29 @@ const style = computed(() => ({
}));
</script>
<template>
<div class="progress-circle">
<svg class="progress-ring" :width="diameter" :height="diameter">
<circle
:class="$style.progressRingCircle"
:stroke-width="strokeWidth"
stroke="#DCDFE6"
fill="transparent"
:r="radius"
v-bind="{ cx, cy }"
/>
<circle
:class="$style.progressRingCircle"
stroke="#5C4EC2"
:stroke-width="strokeWidth"
fill="transparent"
:r="radius"
v-bind="{ cx, cy, style }"
/>
</svg>
</div>
</template>
<style module>
.progressRingCircle {
transition: stroke-dashoffset 0.35s linear;

View File

@@ -1,65 +1,3 @@
<template>
<div :class="classes" v-bind="$attrs">
<table :class="$style.datatable">
<thead :class="$style.datatableHeader">
<tr>
<th
v-for="column in columns"
:key="column.id"
:class="column.classes"
:style="getThStyle(column)"
>
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<template v-for="row in visibleRows">
<slot name="row" :columns="columns" :row="row" :get-td-value="getTdValue">
<tr :key="row.id">
<td v-for="column in columns" :key="column.id" :class="column.classes">
<component :is="column.render" v-if="column.render" :row="row" :column="column" />
<span v-else>{{ getTdValue(row, column) }}</span>
</td>
</tr>
</slot>
</template>
</tbody>
</table>
<div :class="$style.pagination">
<N8nPagination
v-if="totalPages > 1"
background
:pager-count="5"
:page-size="rowsPerPage"
layout="prev, pager, next"
:total="totalRows"
:current-page="currentPage"
@update:current-page="onUpdateCurrentPage"
/>
<div :class="$style.pageSizeSelector">
<N8nSelect
size="mini"
:model-value="rowsPerPage"
teleported
@update:model-value="onRowsPerPageChange"
>
<template #prepend>{{ t('datatable.pageSize') }}</template>
<N8nOption
v-for="size in rowsPerPageOptions"
:key="size"
:label="`${size}`"
:value="size"
/>
<N8nOption :label="`All`" value="*"> </N8nOption>
</N8nSelect>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useCssModule } from 'vue';
import N8nSelect from '../N8nSelect';
@@ -138,6 +76,68 @@ function getThStyle(column: DatatableColumn) {
}
</script>
<template>
<div :class="classes" v-bind="$attrs">
<table :class="$style.datatable">
<thead :class="$style.datatableHeader">
<tr>
<th
v-for="column in columns"
:key="column.id"
:class="column.classes"
:style="getThStyle(column)"
>
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<template v-for="row in visibleRows">
<slot name="row" :columns="columns" :row="row" :get-td-value="getTdValue">
<tr :key="row.id">
<td v-for="column in columns" :key="column.id" :class="column.classes">
<component :is="column.render" v-if="column.render" :row="row" :column="column" />
<span v-else>{{ getTdValue(row, column) }}</span>
</td>
</tr>
</slot>
</template>
</tbody>
</table>
<div :class="$style.pagination">
<N8nPagination
v-if="totalPages > 1"
background
:pager-count="5"
:page-size="rowsPerPage"
layout="prev, pager, next"
:total="totalRows"
:current-page="currentPage"
@update:current-page="onUpdateCurrentPage"
/>
<div :class="$style.pageSizeSelector">
<N8nSelect
size="mini"
:model-value="rowsPerPage"
teleported
@update:model-value="onRowsPerPageChange"
>
<template #prepend>{{ t('datatable.pageSize') }}</template>
<N8nOption
v-for="size in rowsPerPageOptions"
:key="size"
:label="`${size}`"
:value="size"
/>
<N8nOption :label="`All`" value="*"> </N8nOption>
</N8nSelect>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.datatableWrapper {
display: block;

View File

@@ -1,43 +1,3 @@
<template>
<div :class="['n8n-form-box', $style.container]">
<div v-if="title" :class="$style.heading">
<N8nHeading size="xlarge">
{{ title }}
</N8nHeading>
</div>
<div :class="$style.inputsContainer">
<N8nFormInputs
:inputs="inputs"
:event-bus="formBus"
:column-view="true"
@update="onUpdateModelValue"
@submit="onSubmit"
/>
</div>
<div v-if="secondaryButtonText || buttonText" :class="$style.buttonsContainer">
<span v-if="secondaryButtonText" :class="$style.secondaryButtonContainer">
<N8nLink size="medium" theme="text" @click="onSecondaryButtonClick">
{{ secondaryButtonText }}
</N8nLink>
</span>
<N8nButton
v-if="buttonText"
:label="buttonText"
:loading="buttonLoading"
data-test-id="form-submit-button"
size="large"
@click="onButtonClick"
/>
</div>
<div :class="$style.actionContainer">
<N8nLink v-if="redirectText && redirectLink" :to="redirectLink">
{{ redirectText }}
</N8nLink>
</div>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import N8nFormInputs from '../N8nFormInputs';
import N8nHeading from '../N8nHeading';
@@ -80,6 +40,46 @@ const onButtonClick = () => formBus.emit('submit');
const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event);
</script>
<template>
<div :class="['n8n-form-box', $style.container]">
<div v-if="title" :class="$style.heading">
<N8nHeading size="xlarge">
{{ title }}
</N8nHeading>
</div>
<div :class="$style.inputsContainer">
<N8nFormInputs
:inputs="inputs"
:event-bus="formBus"
:column-view="true"
@update="onUpdateModelValue"
@submit="onSubmit"
/>
</div>
<div v-if="secondaryButtonText || buttonText" :class="$style.buttonsContainer">
<span v-if="secondaryButtonText" :class="$style.secondaryButtonContainer">
<N8nLink size="medium" theme="text" @click="onSecondaryButtonClick">
{{ secondaryButtonText }}
</N8nLink>
</span>
<N8nButton
v-if="buttonText"
:label="buttonText"
:loading="buttonLoading"
data-test-id="form-submit-button"
size="large"
@click="onButtonClick"
/>
</div>
<div :class="$style.actionContainer">
<N8nLink v-if="redirectText && redirectLink" :to="redirectLink">
{{ redirectText }}
</N8nLink>
</div>
<slot></slot>
</div>
</template>
<style lang="scss" module>
.heading {
display: flex;

View File

@@ -1,96 +1,3 @@
<template>
<N8nCheckbox
v-if="type === 'checkbox'"
ref="inputRef"
:label="label"
:disabled="disabled"
:label-size="labelSize as CheckboxLabelSizePropType"
:model-value="modelValue as CheckboxModelValuePropType"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
/>
<N8nInputLabel
v-else-if="type === 'toggle'"
:input-name="name"
:label="label"
:tooltip-text="tooltipText"
:required="required && showRequiredAsterisk"
>
<template #content>
{{ tooltipText }}
</template>
<ElSwitch
:model-value="modelValue as SwitchModelValuePropType"
:active-color="activeColor"
:inactive-color="inactiveColor"
@update:model-value="onUpdateModelValue"
></ElSwitch>
</N8nInputLabel>
<N8nInputLabel
v-else
:input-name="name"
:label="label"
:tooltip-text="tooltipText"
:required="required && showRequiredAsterisk"
>
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
<slot v-if="hasDefaultSlot" />
<N8nSelect
v-else-if="type === 'select' || type === 'multi-select'"
ref="inputRef"
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
:model-value="modelValue"
:placeholder="placeholder"
:multiple="type === 'multi-select'"
:disabled="disabled"
:name="name"
:teleported="teleported"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
@blur="onBlur"
>
<N8nOption
v-for="option in options || []"
:key="option.value"
:value="option.value"
:label="option.label"
:disabled="!!option.disabled"
size="small"
/>
</N8nSelect>
<N8nInput
v-else
ref="inputRef"
:name="name"
:type="type as InputTypePropType"
:placeholder="placeholder"
:model-value="modelValue as InputModelValuePropType"
:maxlength="maxlength"
:autocomplete="autocomplete"
:disabled="disabled"
@update:model-value="onUpdateModelValue"
@blur="onBlur"
@focus="onFocus"
/>
</div>
<div v-if="showErrors" :class="$style.errors">
<span v-text="validationError" />
<n8n-link
v-if="documentationUrl && documentationText"
:to="documentationUrl"
:new-window="true"
size="small"
theme="danger"
>
{{ documentationText }}
</n8n-link>
</div>
<div v-else-if="infoText" :class="$style.infoText">
<span size="small" v-text="infoText" />
</div>
</N8nInputLabel>
</template>
<script lang="ts" setup>
import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue';
@@ -271,6 +178,99 @@ watch(
defineExpose({ inputRef });
</script>
<template>
<N8nCheckbox
v-if="type === 'checkbox'"
ref="inputRef"
:label="label"
:disabled="disabled"
:label-size="labelSize as CheckboxLabelSizePropType"
:model-value="modelValue as CheckboxModelValuePropType"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
/>
<N8nInputLabel
v-else-if="type === 'toggle'"
:input-name="name"
:label="label"
:tooltip-text="tooltipText"
:required="required && showRequiredAsterisk"
>
<template #content>
{{ tooltipText }}
</template>
<ElSwitch
:model-value="modelValue as SwitchModelValuePropType"
:active-color="activeColor"
:inactive-color="inactiveColor"
@update:model-value="onUpdateModelValue"
></ElSwitch>
</N8nInputLabel>
<N8nInputLabel
v-else
:input-name="name"
:label="label"
:tooltip-text="tooltipText"
:required="required && showRequiredAsterisk"
>
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
<slot v-if="hasDefaultSlot" />
<N8nSelect
v-else-if="type === 'select' || type === 'multi-select'"
ref="inputRef"
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
:model-value="modelValue"
:placeholder="placeholder"
:multiple="type === 'multi-select'"
:disabled="disabled"
:name="name"
:teleported="teleported"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
@blur="onBlur"
>
<N8nOption
v-for="option in options || []"
:key="option.value"
:value="option.value"
:label="option.label"
:disabled="!!option.disabled"
size="small"
/>
</N8nSelect>
<N8nInput
v-else
ref="inputRef"
:name="name"
:type="type as InputTypePropType"
:placeholder="placeholder"
:model-value="modelValue as InputModelValuePropType"
:maxlength="maxlength"
:autocomplete="autocomplete"
:disabled="disabled"
@update:model-value="onUpdateModelValue"
@blur="onBlur"
@focus="onFocus"
/>
</div>
<div v-if="showErrors" :class="$style.errors">
<span v-text="validationError" />
<n8n-link
v-if="documentationUrl && documentationText"
:to="documentationUrl"
:new-window="true"
size="small"
theme="danger"
>
{{ documentationText }}
</n8n-link>
</div>
<div v-else-if="infoText" :class="$style.infoText">
<span size="small" v-text="infoText" />
</div>
</N8nInputLabel>
</template>
<style lang="scss" module>
.infoText {
margin-top: var(--spacing-2xs);

View File

@@ -1,9 +1,3 @@
<template>
<component :is="tag" :class="['n8n-heading', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
@@ -50,6 +44,12 @@ const classes = computed(() => {
});
</script>
<template>
<component :is="tag" :class="['n8n-heading', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<style lang="scss" module>
.bold {
font-weight: var(--font-weight-bold);

View File

@@ -1,9 +1,3 @@
<template>
<N8nText :size="size" :color="color" :compact="true" class="n8n-icon" v-bind="$attrs">
<FontAwesomeIcon :icon="icon" :spin="spin" :class="$style[size]" />
</N8nText>
</template>
<script lang="ts" setup>
import type { FontAwesomeIconProps } from '@fortawesome/vue-fontawesome';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@@ -24,6 +18,12 @@ withDefaults(defineProps<IconProps>(), {
});
</script>
<template>
<N8nText :size="size" :color="color" :compact="true" class="n8n-icon" v-bind="$attrs">
<FontAwesomeIcon :icon="icon" :spin="spin" :class="$style[size]" />
</N8nText>
</template>
<style lang="scss" module>
.xlarge {
width: var(--font-size-xl) !important;

View File

@@ -1,7 +1,3 @@
<template>
<N8nButton square v-bind="{ ...$attrs, ...$props }" />
</template>
<script lang="ts" setup>
import type { IconButtonProps } from 'n8n-design-system/types/button';
import N8nButton from '../N8nButton';
@@ -17,3 +13,7 @@ withDefaults(defineProps<IconButtonProps>(), {
active: false,
});
</script>
<template>
<N8nButton square v-bind="{ ...$attrs, ...$props }" />
</template>

View File

@@ -1,43 +1,3 @@
<template>
<div :class="['accordion', $style.container]">
<div :class="{ [$style.header]: true, [$style.expanded]: expanded }" @click="toggle">
<N8nIcon
v-if="headerIcon"
:icon="headerIcon.icon"
:color="headerIcon.color"
size="small"
class="mr-2xs"
/>
<N8nText :class="$style.headerText" color="text-base" size="small" align="left" bold>{{
title
}}</N8nText>
<N8nIcon :icon="expanded ? 'chevron-up' : 'chevron-down'" bold />
</div>
<div
v-if="expanded"
:class="{ [$style.description]: true, [$style.collapsed]: !expanded }"
@click="onClick"
>
<!-- Info accordion can display list of items with icons or just a HTML description -->
<div v-if="items.length > 0" :class="$style.accordionItems">
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip">
<template #content>
<div @click="onTooltipClick(item.id, $event)" v-html="item.tooltip"></div>
</template>
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip>
<N8nText size="small" color="text-base">{{ item.label }}</N8nText>
</div>
</div>
<N8nText color="text-base" size="small" align="left">
<span v-html="description"></span>
</N8nText>
<slot name="customContent"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import N8nText from '../N8nText';
@@ -90,6 +50,46 @@ const onClick = (e: MouseEvent) => emit('click:body', e);
const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick', item, event);
</script>
<template>
<div :class="['accordion', $style.container]">
<div :class="{ [$style.header]: true, [$style.expanded]: expanded }" @click="toggle">
<N8nIcon
v-if="headerIcon"
:icon="headerIcon.icon"
:color="headerIcon.color"
size="small"
class="mr-2xs"
/>
<N8nText :class="$style.headerText" color="text-base" size="small" align="left" bold>{{
title
}}</N8nText>
<N8nIcon :icon="expanded ? 'chevron-up' : 'chevron-down'" bold />
</div>
<div
v-if="expanded"
:class="{ [$style.description]: true, [$style.collapsed]: !expanded }"
@click="onClick"
>
<!-- Info accordion can display list of items with icons or just a HTML description -->
<div v-if="items.length > 0" :class="$style.accordionItems">
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip">
<template #content>
<div @click="onTooltipClick(item.id, $event)" v-html="item.tooltip"></div>
</template>
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip>
<N8nText size="small" color="text-base">{{ item.label }}</N8nText>
</div>
</div>
<N8nText color="text-base" size="small" align="left">
<span v-html="description"></span>
</N8nText>
<slot name="customContent"></slot>
</div>
</div>
</template>
<style lang="scss" module>
.container {
background-color: var(--color-background-base);

View File

@@ -1,37 +1,3 @@
<template>
<div
:class="{
'n8n-info-tip': true,
[$style.infoTip]: true,
[$style[theme]]: true,
[$style[type]]: true,
[$style.bold]: bold,
}"
>
<N8nTooltip
v-if="type === 'tooltip'"
:placement="tooltipPlacement"
:popper-class="$style.tooltipPopper"
:disabled="type !== 'tooltip'"
>
<span :class="$style.iconText" :style="{ color: iconData.color }">
<N8nIcon :icon="iconData.icon" />
</span>
<template #content>
<span>
<slot />
</span>
</template>
</N8nTooltip>
<span v-else :class="$style.iconText">
<N8nIcon :icon="iconData.icon" />
<span>
<slot />
</span>
</span>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { Placement } from 'element-plus';
@@ -92,6 +58,40 @@ const iconData = computed((): { icon: string; color: string } => {
});
</script>
<template>
<div
:class="{
'n8n-info-tip': true,
[$style.infoTip]: true,
[$style[theme]]: true,
[$style[type]]: true,
[$style.bold]: bold,
}"
>
<N8nTooltip
v-if="type === 'tooltip'"
:placement="tooltipPlacement"
:popper-class="$style.tooltipPopper"
:disabled="type !== 'tooltip'"
>
<span :class="$style.iconText" :style="{ color: iconData.color }">
<N8nIcon :icon="iconData.icon" />
</span>
<template #content>
<span>
<slot />
</span>
</template>
</N8nTooltip>
<span v-else :class="$style.iconText">
<N8nIcon :icon="iconData.icon" />
<span>
<slot />
</span>
</span>
</div>
</template>
<style lang="scss" module>
.infoTip {
display: flex;

View File

@@ -1,35 +1,3 @@
<template>
<ElInput
ref="innerInput"
:model-value="modelValue"
:type="type"
:size="resolvedSize"
:class="['n8n-input', ...classes]"
:autocomplete="autocomplete"
:name="name"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:clearable="clearable"
:rows="rows"
:title="title"
v-bind="$attrs"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template v-if="$slots.append" #append>
<slot name="append" />
</template>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix" />
</template>
<template v-if="$slots.suffix" #suffix>
<slot name="suffix" />
</template>
</ElInput>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { ElInput } from 'element-plus';
@@ -96,6 +64,38 @@ const select = () => inputElement.value?.select();
defineExpose({ focus, blur, select });
</script>
<template>
<ElInput
ref="innerInput"
:model-value="modelValue"
:type="type"
:size="resolvedSize"
:class="['n8n-input', ...classes]"
:autocomplete="autocomplete"
:name="name"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:clearable="clearable"
:rows="rows"
:title="title"
v-bind="$attrs"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template v-if="$slots.append" #append>
<slot name="append" />
</template>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix" />
</template>
<template v-if="$slots.suffix" #suffix>
<slot name="suffix" />
</template>
</ElInput>
</template>
<style lang="scss" module>
.xlarge {
--input-font-size: var(--font-size-m);

View File

@@ -1,3 +1,36 @@
<script lang="ts" setup>
import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon';
import N8nTooltip from '../N8nTooltip';
import type { TextColor } from 'n8n-design-system/types/text';
const SIZE = ['small', 'medium'] as const;
interface InputLabelProps {
compact?: boolean;
color?: TextColor;
label?: string;
tooltipText?: string;
inputName?: string;
required?: boolean;
bold?: boolean;
size?: (typeof SIZE)[number];
underline?: boolean;
showTooltip?: boolean;
showOptions?: boolean;
}
defineOptions({ name: 'N8nInputLabel' });
withDefaults(defineProps<InputLabelProps>(), {
compact: false,
bold: true,
size: 'medium',
});
const addTargetBlank = (html: string) =>
html && html.includes('href=') ? html.replace(/href=/g, 'target="_blank" href=') : html;
</script>
<template>
<div :class="$style.container" v-bind="$attrs" data-test-id="input-label">
<label
@@ -45,39 +78,6 @@
</div>
</template>
<script lang="ts" setup>
import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon';
import N8nTooltip from '../N8nTooltip';
import type { TextColor } from 'n8n-design-system/types/text';
const SIZE = ['small', 'medium'] as const;
interface InputLabelProps {
compact?: boolean;
color?: TextColor;
label?: string;
tooltipText?: string;
inputName?: string;
required?: boolean;
bold?: boolean;
size?: (typeof SIZE)[number];
underline?: boolean;
showTooltip?: boolean;
showOptions?: boolean;
}
defineOptions({ name: 'N8nInputLabel' });
withDefaults(defineProps<InputLabelProps>(), {
compact: false,
bold: true,
size: 'medium',
});
const addTargetBlank = (html: string) =>
html && html.includes('href=') ? html.replace(/href=/g, 'target="_blank" href=') : html;
</script>
<style lang="scss" module>
.container {
display: flex;

View File

@@ -1,13 +1,3 @@
<template>
<N8nRoute :to="to" :new-window="newWindow" v-bind="$attrs" class="n8n-link">
<span :class="$style[`${underline ? `${theme}-underline` : theme}`]">
<N8nText :size="size" :bold="bold">
<slot></slot>
</N8nText>
</span>
</N8nRoute>
</template>
<script lang="ts" setup>
import type { RouteLocationRaw } from 'vue-router';
import N8nText from '../N8nText';
@@ -35,6 +25,16 @@ withDefaults(defineProps<LinkProps>(), {
});
</script>
<template>
<N8nRoute :to="to" :new-window="newWindow" v-bind="$attrs" class="n8n-link">
<span :class="$style[`${underline ? `${theme}-underline` : theme}`]">
<N8nText :size="size" :bold="bold">
<slot></slot>
</N8nText>
</span>
</N8nRoute>
</template>
<style lang="scss" module>
@import '../../utils';
@import '../../css/common/var';

View File

@@ -1,3 +1,37 @@
<script lang="ts" setup>
import { ElSkeleton, ElSkeletonItem } from 'element-plus';
const VARIANT = [
'custom',
'p',
'text',
'h1',
'h3',
'text',
'caption',
'button',
'image',
'circle',
'rect',
] as const;
interface LoadingProps {
animated?: boolean;
loading?: boolean;
rows?: number;
shrinkLast?: boolean;
variant?: (typeof VARIANT)[number];
}
withDefaults(defineProps<LoadingProps>(), {
animated: true,
loading: true,
rows: 1,
shrinkLast: true,
variant: 'p',
});
</script>
<template>
<ElSkeleton
:loading="loading"
@@ -35,40 +69,6 @@
</ElSkeleton>
</template>
<script lang="ts" setup>
import { ElSkeleton, ElSkeletonItem } from 'element-plus';
const VARIANT = [
'custom',
'p',
'text',
'h1',
'h3',
'text',
'caption',
'button',
'image',
'circle',
'rect',
] as const;
interface LoadingProps {
animated?: boolean;
loading?: boolean;
rows?: number;
shrinkLast?: boolean;
variant?: (typeof VARIANT)[number];
}
withDefaults(defineProps<LoadingProps>(), {
animated: true,
loading: true,
rows: 1,
shrinkLast: true,
variant: 'p',
});
</script>
<style lang="scss" module>
.h1Last {
width: 40%;

View File

@@ -1,23 +1,3 @@
<template>
<div class="n8n-markdown">
<div
v-if="!loading"
ref="editor"
:class="$style[theme]"
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
v-html="htmlContent"
/>
<div v-else :class="$style.markdown">
<div v-for="(_, index) in loadingBlocks" :key="index">
<N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" />
<div :class="$style.spacer" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import type { Options as MarkdownOptions } from 'markdown-it';
@@ -213,6 +193,26 @@ const onCheckboxChange = (index: number) => {
};
</script>
<template>
<div class="n8n-markdown">
<div
v-if="!loading"
ref="editor"
:class="$style[theme]"
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
v-html="htmlContent"
/>
<div v-else :class="$style.markdown">
<div v-for="(_, index) in loadingBlocks" :key="index">
<N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" />
<div :class="$style.spacer" />
</div>
</div>
</div>
</template>
<style lang="scss" module>
.markdown {
color: var(--color-text-base);

View File

@@ -1,58 +1,3 @@
<template>
<div
:class="{
['menu-container']: true,
[$style.container]: true,
[$style.menuCollapsed]: collapsed,
[$style.transparentBackground]: transparentBackground,
}"
>
<div v-if="$slots.header" :class="$style.menuHeader">
<slot name="header"></slot>
</div>
<div :class="$style.menuContent">
<div :class="{ [$style.upperContent]: true, ['pt-xs']: $slots.menuPrefix }">
<div v-if="$slots.menuPrefix" :class="$style.menuPrefix">
<slot name="menuPrefix"></slot>
</div>
<ElMenu :default-active="defaultActive" :collapse="collapsed">
<N8nMenuItem
v-for="item in upperMenuItems"
:key="item.id"
:item="item"
:compact="collapsed"
:tooltip-delay="tooltipDelay"
:mode="mode"
:active-tab="activeTab"
:handle-select="onSelect"
/>
</ElMenu>
</div>
<div :class="[$style.lowerContent, 'pb-2xs']">
<slot name="beforeLowerMenu"></slot>
<ElMenu :default-active="defaultActive" :collapse="collapsed">
<N8nMenuItem
v-for="item in lowerMenuItems"
:key="item.id"
:item="item"
:compact="collapsed"
:tooltip-delay="tooltipDelay"
:mode="mode"
:active-tab="activeTab"
:handle-select="onSelect"
/>
</ElMenu>
<div v-if="$slots.menuSuffix" :class="$style.menuSuffix">
<slot name="menuSuffix"></slot>
</div>
</div>
</div>
<div v-if="$slots.footer" :class="$style.menuFooter">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
@@ -125,6 +70,61 @@ const onSelect = (item: IMenuItem): void => {
};
</script>
<template>
<div
:class="{
['menu-container']: true,
[$style.container]: true,
[$style.menuCollapsed]: collapsed,
[$style.transparentBackground]: transparentBackground,
}"
>
<div v-if="$slots.header" :class="$style.menuHeader">
<slot name="header"></slot>
</div>
<div :class="$style.menuContent">
<div :class="{ [$style.upperContent]: true, ['pt-xs']: $slots.menuPrefix }">
<div v-if="$slots.menuPrefix" :class="$style.menuPrefix">
<slot name="menuPrefix"></slot>
</div>
<ElMenu :default-active="defaultActive" :collapse="collapsed">
<N8nMenuItem
v-for="item in upperMenuItems"
:key="item.id"
:item="item"
:compact="collapsed"
:tooltip-delay="tooltipDelay"
:mode="mode"
:active-tab="activeTab"
:handle-select="onSelect"
/>
</ElMenu>
</div>
<div :class="[$style.lowerContent, 'pb-2xs']">
<slot name="beforeLowerMenu"></slot>
<ElMenu :default-active="defaultActive" :collapse="collapsed">
<N8nMenuItem
v-for="item in lowerMenuItems"
:key="item.id"
:item="item"
:compact="collapsed"
:tooltip-delay="tooltipDelay"
:mode="mode"
:active-tab="activeTab"
:handle-select="onSelect"
/>
</ElMenu>
<div v-if="$slots.menuSuffix" :class="$style.menuSuffix">
<slot name="menuSuffix"></slot>
</div>
</div>
</div>
<div v-if="$slots.footer" :class="$style.menuFooter">
<slot name="footer"></slot>
</div>
</div>
</template>
<style lang="scss" module>
.container {
height: 100%;

View File

@@ -1,3 +1,67 @@
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import { useRoute } from 'vue-router';
import { ElSubMenu, ElMenuItem } from 'element-plus';
import N8nTooltip from '../N8nTooltip';
import N8nIcon from '../N8nIcon';
import ConditionalRouterLink from '../ConditionalRouterLink';
import type { IMenuItem } from '../../types';
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
import { getInitials } from '../../utils/labelUtil';
interface MenuItemProps {
item: IMenuItem;
compact?: boolean;
tooltipDelay?: number;
popperClass?: string;
mode?: 'router' | 'tabs';
activeTab?: string;
handleSelect?: (item: IMenuItem) => void;
}
const props = withDefaults(defineProps<MenuItemProps>(), {
compact: false,
tooltipDelay: 300,
popperClass: '',
mode: 'router',
});
const $style = useCssModule();
const $route = useRoute();
const availableChildren = computed((): IMenuItem[] =>
Array.isArray(props.item.children)
? props.item.children.filter((child) => child.available !== false)
: [],
);
const currentRoute = computed(() => {
return $route ?? { name: '', path: '' };
});
const submenuPopperClass = computed((): string => {
const popperClass = [$style.submenuPopper, props.popperClass];
if (props.compact) {
popperClass.push($style.compact);
}
return popperClass.join(' ');
});
const isActive = (item: IMenuItem): boolean => {
if (props.mode === 'router') {
return doesMenuItemMatchCurrentRoute(item, currentRoute.value);
} else {
return item.id === props.activeTab;
}
};
const isItemActive = (item: IMenuItem): boolean => {
const hasActiveChild =
Array.isArray(item.children) && item.children.some((child) => isActive(child));
return isActive(item) || hasActiveChild;
};
</script>
<template>
<div :class="['n8n-menu-item', $style.item]">
<ElSubMenu
@@ -88,70 +152,6 @@
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import { useRoute } from 'vue-router';
import { ElSubMenu, ElMenuItem } from 'element-plus';
import N8nTooltip from '../N8nTooltip';
import N8nIcon from '../N8nIcon';
import ConditionalRouterLink from '../ConditionalRouterLink';
import type { IMenuItem } from '../../types';
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
import { getInitials } from '../../utils/labelUtil';
interface MenuItemProps {
item: IMenuItem;
compact?: boolean;
tooltipDelay?: number;
popperClass?: string;
mode?: 'router' | 'tabs';
activeTab?: string;
handleSelect?: (item: IMenuItem) => void;
}
const props = withDefaults(defineProps<MenuItemProps>(), {
compact: false,
tooltipDelay: 300,
popperClass: '',
mode: 'router',
});
const $style = useCssModule();
const $route = useRoute();
const availableChildren = computed((): IMenuItem[] =>
Array.isArray(props.item.children)
? props.item.children.filter((child) => child.available !== false)
: [],
);
const currentRoute = computed(() => {
return $route ?? { name: '', path: '' };
});
const submenuPopperClass = computed((): string => {
const popperClass = [$style.submenuPopper, props.popperClass];
if (props.compact) {
popperClass.push($style.compact);
}
return popperClass.join(' ');
});
const isActive = (item: IMenuItem): boolean => {
if (props.mode === 'router') {
return doesMenuItemMatchCurrentRoute(item, currentRoute.value);
} else {
return item.id === props.activeTab;
}
};
const isItemActive = (item: IMenuItem): boolean => {
const hasActiveChild =
Array.isArray(item.children) && item.children.some((child) => isActive(child));
return isActive(item) || hasActiveChild;
};
</script>
<style module lang="scss">
// Element menu-item overrides
:global(.el-menu-item),

View File

@@ -1,40 +1,3 @@
<template>
<div class="n8n-node-icon" v-bind="$attrs">
<div
:class="{
[$style.nodeIconWrapper]: true,
[$style.circle]: circle,
[$style.disabled]: disabled,
}"
:style="iconStyleData"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<N8nTooltip v-if="showTooltip" :placement="tooltipPosition" :disabled="!showTooltip">
<template #content>{{ nodeTypeName }}</template>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :class="$style.iconFa" :style="fontStyleData" />
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</N8nTooltip>
<template v-else>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<n8n-node-icon :type="badge.type" :src="badge.src" :size="badgeSize"></n8n-node-icon>
</div>
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@@ -107,6 +70,43 @@ const badgeStyleData = computed((): Record<string, string> => {
});
</script>
<template>
<div class="n8n-node-icon" v-bind="$attrs">
<div
:class="{
[$style.nodeIconWrapper]: true,
[$style.circle]: circle,
[$style.disabled]: disabled,
}"
:style="iconStyleData"
>
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
<N8nTooltip v-if="showTooltip" :placement="tooltipPosition" :disabled="!showTooltip">
<template #content>{{ nodeTypeName }}</template>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :class="$style.iconFa" :style="fontStyleData" />
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</N8nTooltip>
<template v-else>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<n8n-node-icon :type="badge.type" :src="badge.src" :size="badgeSize"></n8n-node-icon>
</div>
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
</div>
</template>
</div>
</div>
</template>
<style lang="scss" module>
.nodeIconWrapper {
width: var(--node-icon-size, 26px);

View File

@@ -1,20 +1,3 @@
<template>
<div :id="id" :class="classes" role="alert" @click="onClick">
<div class="notice-content">
<N8nText size="small" :compact="true">
<slot>
<span
:id="`${id}-content`"
:class="showFullContent ? $style['expanded'] : $style['truncated']"
role="region"
v-html="displayContent"
/>
</slot>
</N8nText>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useCssModule } from 'vue';
import sanitize from 'sanitize-html';
@@ -81,6 +64,23 @@ const onClick = (event: MouseEvent) => {
};
</script>
<template>
<div :id="id" :class="classes" role="alert" @click="onClick">
<div class="notice-content">
<N8nText size="small" :compact="true">
<slot>
<span
:id="`${id}-content`"
:class="showFullContent ? $style['expanded'] : $style['truncated']"
role="region"
v-html="displayContent"
/>
</slot>
</N8nText>
</div>
</div>
</template>
<style lang="scss" module>
.notice {
font-size: var(--font-size-2xs);

View File

@@ -1,3 +1,7 @@
<script lang="ts" setup>
defineOptions({ name: 'N8nPulse' });
</script>
<template>
<div :class="['pulse', $style.pulseContainer]">
<div :class="$style.pulse">
@@ -8,10 +12,6 @@
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'N8nPulse' });
</script>
<style lang="scss" module>
$--light-pulse-color: hsla(
var(--color-primary-h),

View File

@@ -1,3 +1,19 @@
<script lang="ts" setup>
interface RadioButtonProps {
label: string;
value: string;
active?: boolean;
disabled?: boolean;
size?: 'small' | 'medium';
}
withDefaults(defineProps<RadioButtonProps>(), {
active: false,
disabled: false,
size: 'medium',
});
</script>
<template>
<label
role="radio"
@@ -23,22 +39,6 @@
</label>
</template>
<script lang="ts" setup>
interface RadioButtonProps {
label: string;
value: string;
active?: boolean;
disabled?: boolean;
size?: 'small' | 'medium';
}
withDefaults(defineProps<RadioButtonProps>(), {
active: false,
disabled: false,
size: 'medium',
});
</script>
<style lang="scss" module>
.container {
display: inline-block;

View File

@@ -1,20 +1,3 @@
<template>
<div
role="radiogroup"
:class="{ 'n8n-radio-buttons': true, [$style.radioGroup]: true, [$style.disabled]: disabled }"
>
<RadioButton
v-for="option in options"
:key="option.value"
v-bind="option"
:active="modelValue === option.value"
:size="size"
:disabled="disabled || option.disabled"
@click.prevent.stop="onClick(option, $event)"
/>
</div>
</template>
<script lang="ts" setup>
import RadioButton from './RadioButton.vue';
@@ -53,6 +36,23 @@ const onClick = (
};
</script>
<template>
<div
role="radiogroup"
:class="{ 'n8n-radio-buttons': true, [$style.radioGroup]: true, [$style.disabled]: disabled }"
>
<RadioButton
v-for="option in options"
:key="option.value"
v-bind="option"
:active="modelValue === option.value"
:size="size"
:disabled="disabled || option.disabled"
@click.prevent.stop="onClick(option, $event)"
/>
</div>
</template>
<style lang="scss" module>
.radioGroup {
display: inline-flex;

View File

@@ -1,16 +1,3 @@
<template>
<div :class="$style.resize">
<div
v-for="direction in enabledDirections"
:key="direction"
:data-dir="direction"
:class="{ [$style.resizer]: true, [$style[direction]]: true }"
@mousedown="resizerMove"
/>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
@@ -180,6 +167,19 @@ const resizerMove = (event: MouseEvent) => {
};
</script>
<template>
<div :class="$style.resize">
<div
v-for="direction in enabledDirections"
:key="direction"
:data-dir="direction"
:class="{ [$style.resizer]: true, [$style[direction]]: true }"
@mousedown="resizerMove"
/>
<slot></slot>
</div>
</template>
<style lang="scss" module>
.resize {
position: relative;

View File

@@ -1,20 +1,3 @@
<template>
<N8nResizeWrapper
:is-resizing-enabled="!readOnly"
:height="height"
:width="width"
:min-height="minHeight"
:min-width="minWidth"
:scale="scale"
:grid-size="gridSize"
@resizeend="onResizeEnd"
@resize="onResize"
@resizestart="onResizeStart"
>
<N8nSticky v-bind="stickyBindings" />
</N8nResizeWrapper>
</template>
<script lang="ts" setup>
import { computed, ref, useAttrs } from 'vue';
import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
@@ -59,3 +42,20 @@ const onResizeEnd = () => {
emit('resizeend');
};
</script>
<template>
<N8nResizeWrapper
:is-resizing-enabled="!readOnly"
:height="height"
:width="width"
:min-height="minHeight"
:min-width="minWidth"
:scale="scale"
:grid-size="gridSize"
@resizeend="onResizeEnd"
@resize="onResize"
@resizestart="onResizeStart"
>
<N8nSticky v-bind="stickyBindings" />
</N8nResizeWrapper>
</template>

View File

@@ -1,17 +1,3 @@
<template>
<router-link v-if="useRouterLink && to" :to="to" v-bind="$attrs">
<slot></slot>
</router-link>
<a
v-else
:href="to ? `${to}` : undefined"
:target="openNewWindow ? '_blank' : '_self'"
v-bind="$attrs"
>
<slot></slot>
</a>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { type RouteLocationRaw } from 'vue-router';
@@ -39,3 +25,17 @@ const useRouterLink = computed(() => {
const openNewWindow = computed(() => !useRouterLink.value);
</script>
<template>
<router-link v-if="useRouterLink && to" :to="to" v-bind="$attrs">
<slot></slot>
</router-link>
<a
v-else
:href="to ? `${to}` : undefined"
:target="openNewWindow ? '_blank' : '_self'"
v-bind="$attrs"
>
<slot></slot>
</a>
</template>

View File

@@ -1,15 +1,3 @@
<template>
<span class="n8n-spinner">
<div v-if="type === 'ring'" class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<N8nIcon v-else icon="spinner" :size="size" spin />
</span>
</template>
<script lang="ts" setup>
import type { TextSize } from 'n8n-design-system/types/text';
import N8nIcon from '../N8nIcon';
@@ -28,6 +16,18 @@ withDefaults(defineProps<SpinnerProps>(), {
});
</script>
<template>
<span class="n8n-spinner">
<div v-if="type === 'ring'" class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<N8nIcon v-else icon="spinner" :size="size" spin />
</span>
</template>
<style lang="scss">
.lds-ring {
display: inline-block;

View File

@@ -1,51 +1,3 @@
<template>
<div
:class="{
'n8n-sticky': true,
[$style.sticky]: true,
[$style.clickable]: !isResizing,
[$style[`color-${backgroundColor}`]]: true,
}"
:style="styles"
@keydown.prevent
>
<div v-show="!editMode" :class="$style.wrapper" @dblclick.stop="onDoubleClick">
<N8nMarkdown
theme="sticky"
:content="modelValue"
:with-multi-breaks="true"
@markdown-click="onMarkdownClick"
@update-content="onUpdateModelValue"
/>
</div>
<div
v-show="editMode"
:class="{ 'full-height': !shouldShowFooter, 'sticky-textarea': true }"
@click.stop
@mousedown.stop
@mouseup.stop
@keydown.esc="onInputBlur"
@keydown.stop
>
<N8nInput
ref="input"
:model-value="modelValue"
:name="inputName"
type="textarea"
:rows="5"
@blur="onInputBlur"
@update:model-value="onUpdateModelValue"
@wheel="onInputScroll"
/>
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<N8nText size="xsmall" align="right">
<span v-html="t('sticky.markdownHint')"></span>
</N8nText>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import N8nInput from '../N8nInput';
@@ -122,6 +74,54 @@ const onInputScroll = (event: WheelEvent) => {
};
</script>
<template>
<div
:class="{
'n8n-sticky': true,
[$style.sticky]: true,
[$style.clickable]: !isResizing,
[$style[`color-${backgroundColor}`]]: true,
}"
:style="styles"
@keydown.prevent
>
<div v-show="!editMode" :class="$style.wrapper" @dblclick.stop="onDoubleClick">
<N8nMarkdown
theme="sticky"
:content="modelValue"
:with-multi-breaks="true"
@markdown-click="onMarkdownClick"
@update-content="onUpdateModelValue"
/>
</div>
<div
v-show="editMode"
:class="{ 'full-height': !shouldShowFooter, 'sticky-textarea': true }"
@click.stop
@mousedown.stop
@mouseup.stop
@keydown.esc="onInputBlur"
@keydown.stop
>
<N8nInput
ref="input"
:model-value="modelValue"
:name="inputName"
type="textarea"
:rows="5"
@blur="onInputBlur"
@update:model-value="onUpdateModelValue"
@wheel="onInputScroll"
/>
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<N8nText size="xsmall" align="right">
<span v-html="t('sticky.markdownHint')"></span>
</N8nText>
</div>
</div>
</template>
<style lang="scss" module>
.sticky {
position: absolute;

View File

@@ -1,59 +1,3 @@
<template>
<div :class="['n8n-tabs', $style.container]">
<div v-if="scrollPosition > 0" :class="$style.back" @click="scrollLeft">
<N8nIcon icon="chevron-left" size="small" />
</div>
<div v-if="canScrollRight" :class="$style.next" @click="scrollRight">
<N8nIcon icon="chevron-right" size="small" />
</div>
<div ref="tabs" :class="$style.tabs">
<div
v-for="option in options"
:id="option.value"
:key="option.value"
:class="{ [$style.alignRight]: option.align === 'right' }"
>
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
<template #content>
<div @click="handleTooltipClick(option.value, $event)" v-html="option.tooltip" />
</template>
<a
v-if="option.href"
target="_blank"
:href="option.href"
:class="[$style.link, $style.tab]"
@click="() => handleTabClick(option.value)"
>
<div>
{{ option.label }}
<span :class="$style.external">
<N8nIcon icon="external-link-alt" size="xsmall" />
</span>
</div>
</a>
<router-link
v-else-if="option.to"
:to="option.to"
:class="[$style.tab, { [$style.activeTab]: modelValue === option.value }]"
>
<N8nIcon v-if="option.icon" :icon="option.icon" size="medium" />
<span v-if="option.label">{{ option.label }}</span>
</router-link>
<div
v-else
:class="{ [$style.tab]: true, [$style.activeTab]: modelValue === option.value }"
:data-test-id="`tab-${option.value}`"
@click="() => handleTabClick(option.value)"
>
<N8nIcon v-if="option.icon" :icon="option.icon" size="small" />
<span v-if="option.label">{{ option.label }}</span>
</div>
</N8nTooltip>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import N8nIcon from '../N8nIcon';
@@ -128,6 +72,62 @@ const scrollLeft = () => scroll(-50);
const scrollRight = () => scroll(50);
</script>
<template>
<div :class="['n8n-tabs', $style.container]">
<div v-if="scrollPosition > 0" :class="$style.back" @click="scrollLeft">
<N8nIcon icon="chevron-left" size="small" />
</div>
<div v-if="canScrollRight" :class="$style.next" @click="scrollRight">
<N8nIcon icon="chevron-right" size="small" />
</div>
<div ref="tabs" :class="$style.tabs">
<div
v-for="option in options"
:id="option.value"
:key="option.value"
:class="{ [$style.alignRight]: option.align === 'right' }"
>
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
<template #content>
<div @click="handleTooltipClick(option.value, $event)" v-html="option.tooltip" />
</template>
<a
v-if="option.href"
target="_blank"
:href="option.href"
:class="[$style.link, $style.tab]"
@click="() => handleTabClick(option.value)"
>
<div>
{{ option.label }}
<span :class="$style.external">
<N8nIcon icon="external-link-alt" size="xsmall" />
</span>
</div>
</a>
<router-link
v-else-if="option.to"
:to="option.to"
:class="[$style.tab, { [$style.activeTab]: modelValue === option.value }]"
>
<N8nIcon v-if="option.icon" :icon="option.icon" size="medium" />
<span v-if="option.label">{{ option.label }}</span>
</router-link>
<div
v-else
:class="{ [$style.tab]: true, [$style.activeTab]: modelValue === option.value }"
:data-test-id="`tab-${option.value}`"
@click="() => handleTabClick(option.value)"
>
<N8nIcon v-if="option.icon" :icon="option.icon" size="small" />
<span v-if="option.label">{{ option.label }}</span>
</div>
</N8nTooltip>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.container {
position: relative;

View File

@@ -1,9 +1,3 @@
<template>
<span :class="['n8n-tag', $style.tag]" v-bind="$attrs">
{{ text }}
</span>
</template>
<script lang="ts" setup>
interface TagProps {
text: string;
@@ -12,6 +6,12 @@ defineOptions({ name: 'N8nTag' });
defineProps<TagProps>();
</script>
<template>
<span :class="['n8n-tag', $style.tag]" v-bind="$attrs">
{{ text }}
</span>
</template>
<style lang="scss" module>
.tag {
min-width: max-content;

View File

@@ -1,23 +1,3 @@
<template>
<div :class="['n8n-tags', $style.tags]">
<N8nTag
v-for="tag in visibleTags"
:key="tag.id"
:text="tag.name"
@click="emit('click:tag', tag.id, $event)"
/>
<N8nLink
v-if="truncate && !showAll && hiddenTagsLength > 0"
theme="text"
underline
size="small"
@click.stop.prevent="onExpand"
>
{{ t('tags.showMore', [`${hiddenTagsLength}`]) }}
</N8nLink>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import N8nTag from '../N8nTag';
@@ -67,6 +47,26 @@ const onExpand = () => {
};
</script>
<template>
<div :class="['n8n-tags', $style.tags]">
<N8nTag
v-for="tag in visibleTags"
:key="tag.id"
:text="tag.name"
@click="emit('click:tag', tag.id, $event)"
/>
<N8nLink
v-if="truncate && !showAll && hiddenTagsLength > 0"
theme="text"
underline
size="small"
@click.stop.prevent="onExpand"
>
{{ t('tags.showMore', [`${hiddenTagsLength}`]) }}
</N8nLink>
</div>
</template>
<style lang="scss" module>
.tags {
display: inline-flex;

View File

@@ -1,9 +1,3 @@
<template>
<component :is="tag" :class="['n8n-text', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import type { TextSize, TextColor, TextAlign } from 'n8n-design-system/types/text';
@@ -46,6 +40,12 @@ const classes = computed(() => {
});
</script>
<template>
<component :is="tag" :class="['n8n-text', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<style lang="scss" module>
.bold {
font-weight: var(--font-weight-bold);

View File

@@ -1,25 +1,3 @@
<template>
<ElTooltip v-bind="{ ...$props, ...$attrs }" :popper-class="$props.popperClass ?? 'n8n-tooltip'">
<slot />
<template #content>
<slot name="content">
<div v-html="content"></div>
</slot>
<div
v-if="buttons.length"
:class="$style.buttons"
:style="{ justifyContent: justifyButtons }"
>
<N8nButton
v-for="button in buttons"
:key="button.attrs.label"
v-bind="{ ...button.attrs, ...button.listeners }"
/>
</div>
</template>
</ElTooltip>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
@@ -65,6 +43,28 @@ export default defineComponent({
});
</script>
<template>
<ElTooltip v-bind="{ ...$props, ...$attrs }" :popper-class="$props.popperClass ?? 'n8n-tooltip'">
<slot />
<template #content>
<slot name="content">
<div v-html="content"></div>
</slot>
<div
v-if="buttons.length"
:class="$style.buttons"
:style="{ justifyContent: justifyButtons }"
>
<N8nButton
v-for="button in buttons"
:key="button.attrs.label"
v-bind="{ ...button.attrs, ...button.listeners }"
/>
</div>
</template>
</ElTooltip>
</template>
<style lang="scss" module>
.buttons {
display: flex;

View File

@@ -1,31 +1,3 @@
<template>
<div v-if="isObject(value)" class="n8n-tree">
<div v-for="(label, i) in Object.keys(value)" :key="i" :class="classes">
<div v-if="isSimple(value[label])" :class="$style.simple">
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span>
<span>:</span>
<slot v-if="$slots.value" name="value" :value="value[label]" />
<span v-else>{{ value[label] }}</span>
</div>
<div v-else>
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span>
<n8n-tree
:path="getPath(label)"
:depth="depth + 1"
:value="value[label] as Record<string, unknown>"
:node-class="nodeClass"
>
<template v-for="(_, name) in $slots" #[name]="data">
<slot :name="name" v-bind="data"></slot>
</template>
</n8n-tree>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
@@ -85,6 +57,34 @@ const getPath = (key: string): Array<string | number> => {
};
</script>
<template>
<div v-if="isObject(value)" class="n8n-tree">
<div v-for="(label, i) in Object.keys(value)" :key="i" :class="classes">
<div v-if="isSimple(value[label])" :class="$style.simple">
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span>
<span>:</span>
<slot v-if="$slots.value" name="value" :value="value[label]" />
<span v-else>{{ value[label] }}</span>
</div>
<div v-else>
<slot v-if="$slots.label" name="label" :label="label" :path="getPath(label)" />
<span v-else>{{ label }}</span>
<n8n-tree
:path="getPath(label)"
:depth="depth + 1"
:value="value[label] as Record<string, unknown>"
:node-class="nodeClass"
>
<template v-for="(_, name) in $slots" #[name]="data">
<slot :name="name" v-bind="data"></slot>
</template>
</n8n-tree>
</div>
</div>
</div>
</template>
<style lang="scss" module>
$--spacing: var(--spacing-s);

View File

@@ -1,30 +1,3 @@
<template>
<div :class="classes">
<div :class="$style.avatarContainer">
<N8nAvatar :first-name="firstName" :last-name="lastName" />
</div>
<div v-if="isPendingUser" :class="$style.pendingUser">
<N8nText :bold="true">{{ email }}</N8nText>
<span :class="$style.pendingBadge"><N8nBadge :bold="true">Pending</N8nBadge></span>
</div>
<div v-else :class="$style.infoContainer">
<div>
<N8nText :bold="true" color="text-dark">
{{ firstName }} {{ lastName }}
{{ isCurrentUser ? t('nds.userInfo.you') : '' }}
</N8nText>
<span v-if="disabled" :class="$style.pendingBadge">
<N8nBadge :bold="true">Disabled</N8nBadge>
</span>
</div>
<div>
<N8nText data-test-id="user-email" size="small" color="text-light">{{ email }}</N8nText>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText';
@@ -59,6 +32,33 @@ const classes = computed(
);
</script>
<template>
<div :class="classes">
<div :class="$style.avatarContainer">
<N8nAvatar :first-name="firstName" :last-name="lastName" />
</div>
<div v-if="isPendingUser" :class="$style.pendingUser">
<N8nText :bold="true">{{ email }}</N8nText>
<span :class="$style.pendingBadge"><N8nBadge :bold="true">Pending</N8nBadge></span>
</div>
<div v-else :class="$style.infoContainer">
<div>
<N8nText :bold="true" color="text-dark">
{{ firstName }} {{ lastName }}
{{ isCurrentUser ? t('nds.userInfo.you') : '' }}
</N8nText>
<span v-if="disabled" :class="$style.pendingBadge">
<N8nBadge :bold="true">Disabled</N8nBadge>
</span>
</div>
<div>
<N8nText data-test-id="user-email" size="small" color="text-light">{{ email }}</N8nText>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.container {
display: inline-flex;

View File

@@ -1,35 +1,3 @@
<template>
<N8nSelect
data-test-id="user-select-trigger"
v-bind="$attrs"
:model-value="modelValue"
:filterable="true"
:filter-method="setFilter"
:placeholder="placeholder || t('nds.userSelect.selectUser')"
:default-first-option="true"
teleported
:popper-class="$style.limitPopperWidth"
:no-data-text="t('nds.userSelect.noMatchingUsers')"
:size="size"
@blur="onBlur"
@focus="onFocus"
>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix" />
</template>
<N8nOption
v-for="user in sortedUsers"
:key="user.id"
:value="user.id"
:class="$style.itemContainer"
:label="getLabel(user)"
:disabled="user.disabled"
>
<N8nUserInfo v-bind="user" :is-current-user="currentUserId === user.id" />
</N8nOption>
</N8nSelect>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import N8nUserInfo from '../N8nUserInfo';
@@ -112,6 +80,38 @@ const getLabel = (user: IUser) =>
!user.fullName ? user.email : `${user.fullName} (${user.email})`;
</script>
<template>
<N8nSelect
data-test-id="user-select-trigger"
v-bind="$attrs"
:model-value="modelValue"
:filterable="true"
:filter-method="setFilter"
:placeholder="placeholder || t('nds.userSelect.selectUser')"
:default-first-option="true"
teleported
:popper-class="$style.limitPopperWidth"
:no-data-text="t('nds.userSelect.noMatchingUsers')"
:size="size"
@blur="onBlur"
@focus="onFocus"
>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix" />
</template>
<N8nOption
v-for="user in sortedUsers"
:key="user.id"
:value="user.id"
:class="$style.itemContainer"
:label="getLabel(user)"
:disabled="user.disabled"
>
<N8nUserInfo v-bind="user" :is-current-user="currentUserId === user.id" />
</N8nOption>
</N8nSelect>
</template>
<style lang="scss" module>
.itemContainer {
--select-option-padding: var(--spacing-2xs) var(--spacing-s);

View File

@@ -1,39 +1,3 @@
<template>
<div>
<div
v-for="(user, i) in sortedUsers"
:key="user.id"
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
:data-test-id="`user-list-item-${user.email}`"
>
<N8nUserInfo
v-bind="user"
:is-current-user="currentUserId === user.id"
:is-saml-login-enabled="isSamlLoginEnabled"
/>
<div :class="$style.badgeContainer">
<N8nBadge v-if="user.isOwner" theme="tertiary" bold>
{{ t('nds.auth.roles.owner') }}
</N8nBadge>
<slot v-if="!user.isOwner && !readonly" name="actions" :user="user" />
<N8nActionToggle
v-if="
!user.isOwner &&
user.signInType !== 'ldap' &&
!readonly &&
getActions(user).length > 0 &&
actions.length > 0
"
placement="bottom"
:actions="getActions(user)"
theme="dark"
@action="(action: string) => onUserAction(user, action)"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import N8nActionToggle from '../N8nActionToggle';
@@ -115,6 +79,42 @@ const onUserAction = (user: IUser, action: string) =>
});
</script>
<template>
<div>
<div
v-for="(user, i) in sortedUsers"
:key="user.id"
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
:data-test-id="`user-list-item-${user.email}`"
>
<N8nUserInfo
v-bind="user"
:is-current-user="currentUserId === user.id"
:is-saml-login-enabled="isSamlLoginEnabled"
/>
<div :class="$style.badgeContainer">
<N8nBadge v-if="user.isOwner" theme="tertiary" bold>
{{ t('nds.auth.roles.owner') }}
</N8nBadge>
<slot v-if="!user.isOwner && !readonly" name="actions" :user="user" />
<N8nActionToggle
v-if="
!user.isOwner &&
user.signInType !== 'ldap' &&
!readonly &&
getActions(user).length > 0 &&
actions.length > 0
"
placement="bottom"
:actions="getActions(user)"
theme="dark"
@action="(action: string) => onUserAction(user, action)"
/>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.itemContainer {
display: flex;

View File

@@ -1,20 +1,3 @@
<template>
<table :class="$style.table">
<tr>
<th :class="$style.row">Name</th>
<th :class="$style.row">Value</th>
</tr>
<tr
v-for="variable in variables"
:key="variable"
:style="attr ? { [attr]: `var(${variable})` } : {}"
>
<td>{{ variable }}</td>
<td>{{ values[variable] }}</td>
</tr>
</table>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
@@ -64,6 +47,23 @@ onUnmounted(() => {
});
</script>
<template>
<table :class="$style.table">
<tr>
<th :class="$style.row">Name</th>
<th :class="$style.row">Value</th>
</tr>
<tr
v-for="variable in variables"
:key="variable"
:style="attr ? { [attr]: `var(${variable})` } : {}"
>
<td>{{ variable }}</td>
<td>{{ values[variable] }}</td>
</tr>
</table>
</template>
<style lang="scss" module>
.table {
text-align: center;

View File

@@ -1,14 +1,3 @@
<template>
<div>
<div v-for="size in sizes" :key="size" class="spacing-group">
<div class="spacing-example" :class="`${property[0]}${side ? side[0] : ''}-${size}`">
<div class="spacing-box" />
<div class="label">{{ property[0] }}{{ side ? side[0] : '' }}-{{ size }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
@@ -42,6 +31,17 @@ const props = withDefaults(defineProps<SpacingPreviewProps>(), {
const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]);
</script>
<template>
<div>
<div v-for="size in sizes" :key="size" class="spacing-group">
<div class="spacing-example" :class="`${property[0]}${side ? side[0] : ''}-${size}`">
<div class="spacing-box" />
<div class="label">{{ property[0] }}{{ side ? side[0] : '' }}-{{ size }}</div>
</div>
</div>
</div>
</template>
<style lang="scss">
$box-size: 64px;

View File

@@ -1,3 +1,46 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import { ABOUT_MODAL_KEY } from '../constants';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store';
import { useToast } from '@/composables/useToast';
import { useClipboard } from '@/composables/useClipboard';
import { useDebugInfo } from '@/composables/useDebugInfo';
export default defineComponent({
name: 'About',
components: {
Modal,
},
data() {
return {
ABOUT_MODAL_KEY,
modalBus: createEventBus(),
};
},
computed: {
...mapStores(useRootStore, useSettingsStore),
},
methods: {
closeDialog() {
this.modalBus.emit('close');
},
async copyDebugInfoToClipboard() {
useToast().showToast({
title: this.$locale.baseText('about.debug.toast.title'),
message: this.$locale.baseText('about.debug.toast.message'),
type: 'info',
duration: 5000,
});
await useClipboard().copy(useDebugInfo().generateDebugInfo());
},
},
});
</script>
<template>
<Modal
max-width="540px"
@@ -68,49 +111,6 @@
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils';
import Modal from './Modal.vue';
import { ABOUT_MODAL_KEY } from '../constants';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store';
import { useToast } from '@/composables/useToast';
import { useClipboard } from '@/composables/useClipboard';
import { useDebugInfo } from '@/composables/useDebugInfo';
export default defineComponent({
name: 'About',
components: {
Modal,
},
data() {
return {
ABOUT_MODAL_KEY,
modalBus: createEventBus(),
};
},
computed: {
...mapStores(useRootStore, useSettingsStore),
},
methods: {
closeDialog() {
this.modalBus.emit('close');
},
async copyDebugInfoToClipboard() {
useToast().showToast({
title: this.$locale.baseText('about.debug.toast.title'),
message: this.$locale.baseText('about.debug.toast.message'),
type: 'info',
duration: 5000,
});
await useClipboard().copy(useDebugInfo().generateDebugInfo());
},
},
});
</script>
<style module lang="scss">
.container > * {
margin-bottom: var(--spacing-s);

View File

@@ -1,39 +1,3 @@
<template>
<Modal
:name="WORKFLOW_ACTIVE_MODAL_KEY"
:title="$locale.baseText('activationModal.workflowActivated')"
width="460px"
>
<template #content>
<div>
<n8n-text>{{ triggerContent }}</n8n-text>
</div>
<div :class="$style.spaced">
<n8n-text>
<n8n-text :bold="true">
{{ $locale.baseText('activationModal.theseExecutionsWillNotShowUp') }}
</n8n-text>
{{ $locale.baseText('activationModal.butYouCanSeeThem') }}
<a @click="showExecutionsList">
{{ $locale.baseText('activationModal.executionList') }}
</a>
{{ $locale.baseText('activationModal.ifYouChooseTo') }}
<a @click="showSettings">{{ $locale.baseText('activationModal.saveExecutions') }}</a>
</n8n-text>
</div>
</template>
<template #footer="{ close }">
<div :class="$style.footer">
<el-checkbox :model-value="checked" @update:model-value="handleCheckboxChange">{{
$locale.baseText('generic.dontShowAgain')
}}</el-checkbox>
<n8n-button :label="$locale.baseText('activationModal.gotIt')" @click="close" />
</div>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
@@ -134,6 +98,42 @@ export default defineComponent({
});
</script>
<template>
<Modal
:name="WORKFLOW_ACTIVE_MODAL_KEY"
:title="$locale.baseText('activationModal.workflowActivated')"
width="460px"
>
<template #content>
<div>
<n8n-text>{{ triggerContent }}</n8n-text>
</div>
<div :class="$style.spaced">
<n8n-text>
<n8n-text :bold="true">
{{ $locale.baseText('activationModal.theseExecutionsWillNotShowUp') }}
</n8n-text>
{{ $locale.baseText('activationModal.butYouCanSeeThem') }}
<a @click="showExecutionsList">
{{ $locale.baseText('activationModal.executionList') }}
</a>
{{ $locale.baseText('activationModal.ifYouChooseTo') }}
<a @click="showSettings">{{ $locale.baseText('activationModal.saveExecutions') }}</a>
</n8n-text>
</div>
</template>
<template #footer="{ close }">
<div :class="$style.footer">
<el-checkbox :model-value="checked" @update:model-value="handleCheckboxChange">{{
$locale.baseText('generic.dontShowAgain')
}}</el-checkbox>
<n8n-button :label="$locale.baseText('activationModal.gotIt')" @click="close" />
</div>
</template>
</Modal>
</template>
<style lang="scss" module>
.spaced {
margin-top: var(--spacing-2xs);

View File

@@ -1,3 +1,9 @@
<script lang="ts">
export default {
props: ['text', 'type'],
};
</script>
<template>
<el-tag
v-if="type === 'danger'"
@@ -18,12 +24,6 @@
</el-tag>
</template>
<script lang="ts">
export default {
props: ['text', 'type'],
};
</script>
<style lang="scss" module>
.badge {
font-size: 11px;

View File

@@ -1,40 +1,3 @@
<template>
<el-tag :type="theme" :disable-transitions="true" :class="$style.container">
<font-awesome-icon
:icon="theme === 'success' ? 'check-circle' : 'exclamation-triangle'"
:class="theme === 'success' ? $style.icon : $style.dangerIcon"
/>
<div :class="$style.banner">
<div :class="$style.content">
<div>
<span :class="theme === 'success' ? $style.message : $style.dangerMessage">
{{ message }}&nbsp;
</span>
<n8n-link v-if="details && !expanded" :bold="true" size="small" @click="expand">
<span :class="$style.moreDetails">More details</span>
</n8n-link>
</div>
</div>
<slot v-if="$slots.button" name="button" />
<n8n-button
v-else-if="buttonLabel"
:label="buttonLoading && buttonLoadingLabel ? buttonLoadingLabel : buttonLabel"
:title="buttonTitle"
:type="theme"
:loading="buttonLoading"
size="small"
outline
@click.stop="onClick"
/>
</div>
<div v-if="expanded" :class="$style.details">
{{ details }}
</div>
</el-tag>
</template>
<script setup lang="ts">
import { ref } from 'vue';
@@ -72,6 +35,43 @@ const onClick = () => {
};
</script>
<template>
<el-tag :type="theme" :disable-transitions="true" :class="$style.container">
<font-awesome-icon
:icon="theme === 'success' ? 'check-circle' : 'exclamation-triangle'"
:class="theme === 'success' ? $style.icon : $style.dangerIcon"
/>
<div :class="$style.banner">
<div :class="$style.content">
<div>
<span :class="theme === 'success' ? $style.message : $style.dangerMessage">
{{ message }}&nbsp;
</span>
<n8n-link v-if="details && !expanded" :bold="true" size="small" @click="expand">
<span :class="$style.moreDetails">More details</span>
</n8n-link>
</div>
</div>
<slot v-if="$slots.button" name="button" />
<n8n-button
v-else-if="buttonLabel"
:label="buttonLoading && buttonLoadingLabel ? buttonLoadingLabel : buttonLabel"
:title="buttonTitle"
:type="theme"
:loading="buttonLoading"
size="small"
outline
@click.stop="onClick"
/>
</div>
<div v-if="expanded" :class="$style.details">
{{ details }}
</div>
</el-tag>
</template>
<style module lang="scss">
.icon {
position: absolute;

View File

@@ -1,23 +1,3 @@
<template>
<div v-if="windowVisible" :class="['binary-data-window', binaryData?.fileType]">
<n8n-button
size="small"
class="binary-data-window-back"
:title="$locale.baseText('binaryDataDisplay.backToOverviewPage')"
icon="arrow-left"
:label="$locale.baseText('binaryDataDisplay.backToList')"
@click.stop="closeWindow"
/>
<div class="binary-data-window-wrapper">
<div v-if="!binaryData">
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
</div>
<BinaryDataDisplayEmbed v-else :binary-data="binaryData" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { IBinaryData, IRunData } from 'n8n-workflow';
@@ -89,6 +69,26 @@ function closeWindow() {
}
</script>
<template>
<div v-if="windowVisible" :class="['binary-data-window', binaryData?.fileType]">
<n8n-button
size="small"
class="binary-data-window-back"
:title="$locale.baseText('binaryDataDisplay.backToOverviewPage')"
icon="arrow-left"
:label="$locale.baseText('binaryDataDisplay.backToList')"
@click.stop="closeWindow"
/>
<div class="binary-data-window-wrapper">
<div v-if="!binaryData">
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
</div>
<BinaryDataDisplayEmbed v-else :binary-data="binaryData" />
</div>
</div>
</template>
<style lang="scss">
.binary-data-window {
position: absolute;

View File

@@ -1,28 +1,3 @@
<template>
<span>
<div v-if="isLoading">Loading binary data...</div>
<div v-else-if="error">Error loading binary data</div>
<span v-else>
<video v-if="binaryData.fileType === 'video'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</video>
<audio v-else-if="binaryData.fileType === 'audio'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</audio>
<VueJsonPretty
v-else-if="binaryData.fileType === 'json'"
:data="data"
:deep="3"
:show-length="true"
/>
<RunDataHtml v-else-if="binaryData.fileType === 'html'" :input-html="data" />
<embed v-else :src="embedSource" class="binary-data" :class="embedClass" />
</span>
</span>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useWorkflowsStore } from '@/stores/workflows.store';
@@ -75,6 +50,31 @@ onMounted(async () => {
});
</script>
<template>
<span>
<div v-if="isLoading">Loading binary data...</div>
<div v-else-if="error">Error loading binary data</div>
<span v-else>
<video v-if="binaryData.fileType === 'video'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</video>
<audio v-else-if="binaryData.fileType === 'audio'" controls autoplay>
<source :src="embedSource" :type="binaryData.mimeType" />
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
</audio>
<VueJsonPretty
v-else-if="binaryData.fileType === 'json'"
:data="data"
:deep="3"
:show-length="true"
/>
<RunDataHtml v-else-if="binaryData.fileType === 'html'" :input-html="data" />
<embed v-else :src="embedSource" class="binary-data" :class="embedClass" />
</span>
</span>
</template>
<style lang="scss">
.binary-data {
background-color: var(--color-foreground-xlight);

View File

@@ -1,9 +1,3 @@
<template>
<span>
<slot :bp="bp" :value="value" />
</span>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/constants';
@@ -90,3 +84,9 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', onResize);
});
</script>
<template>
<span>
<slot :bp="bp" :value="value" />
</span>
</template>

View File

@@ -1,3 +1,37 @@
<script lang="ts" setup>
import { onBeforeMount, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useCanvasStore } from '@/stores/canvas.store';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import { useDeviceSupport } from 'n8n-design-system';
const canvasStore = useCanvasStore();
const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore;
const { nodeViewScale, isDemo } = storeToRefs(canvasStore);
const deviceSupport = useDeviceSupport();
const keyDown = (e: KeyboardEvent) => {
const isCtrlKeyPressed = deviceSupport.isCtrlKeyPressed(e);
if ((e.key === '=' || e.key === '+') && !isCtrlKeyPressed) {
zoomIn();
} else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) {
zoomOut();
} else if (e.key === '0' && !isCtrlKeyPressed) {
resetZoom();
} else if (e.key === '1' && !isCtrlKeyPressed) {
zoomToFit();
}
};
onBeforeMount(() => {
document.addEventListener('keydown', keyDown);
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', keyDown);
});
</script>
<template>
<div
:class="{
@@ -57,39 +91,6 @@
</KeyboardShortcutTooltip>
</div>
</template>
<script lang="ts" setup>
import { onBeforeMount, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useCanvasStore } from '@/stores/canvas.store';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import { useDeviceSupport } from 'n8n-design-system';
const canvasStore = useCanvasStore();
const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore;
const { nodeViewScale, isDemo } = storeToRefs(canvasStore);
const deviceSupport = useDeviceSupport();
const keyDown = (e: KeyboardEvent) => {
const isCtrlKeyPressed = deviceSupport.isCtrlKeyPressed(e);
if ((e.key === '=' || e.key === '+') && !isCtrlKeyPressed) {
zoomIn();
} else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) {
zoomOut();
} else if (e.key === '0' && !isCtrlKeyPressed) {
resetZoom();
} else if (e.key === '1' && !isCtrlKeyPressed) {
zoomToFit();
}
};
onBeforeMount(() => {
document.addEventListener('keydown', keyDown);
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', keyDown);
});
</script>
<style lang="scss" module>
.zoomMenu {

View File

@@ -1,33 +1,3 @@
<template>
<Modal
:name="CHANGE_PASSWORD_MODAL_KEY"
:title="i18n.baseText('auth.changePassword')"
:center="true"
width="460px"
:event-bus="modalBus"
@enter="onSubmit"
>
<template #content>
<n8n-form-inputs
:inputs="config"
:event-bus="formBus"
:column-view="true"
@update="onInput"
@submit="onSubmit"
/>
</template>
<template #footer>
<n8n-button
:loading="loading"
:label="i18n.baseText('auth.changePassword')"
float="right"
data-test-id="change-password-button"
@click="onSubmitClick"
/>
</template>
</Modal>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useToast } from '@/composables/useToast';
@@ -160,3 +130,33 @@ onMounted(() => {
config.value = form;
});
</script>
<template>
<Modal
:name="CHANGE_PASSWORD_MODAL_KEY"
:title="i18n.baseText('auth.changePassword')"
:center="true"
width="460px"
:event-bus="modalBus"
@enter="onSubmit"
>
<template #content>
<n8n-form-inputs
:inputs="config"
:event-bus="formBus"
:column-view="true"
@update="onInput"
@submit="onSubmit"
/>
</template>
<template #footer>
<n8n-button
:loading="loading"
:label="i18n.baseText('auth.changePassword')"
float="right"
data-test-id="change-password-button"
@click="onSubmitClick"
/>
</template>
</Modal>
</template>

View File

@@ -1,51 +1,3 @@
<template>
<div
ref="codeNodeEditorContainerRef"
:class="['code-node-editor', $style['code-node-editor-container'], language]"
@mouseover="onMouseOver"
@mouseout="onMouseOut"
>
<el-tabs
v-if="aiEnabled"
ref="tabs"
v-model="activeTab"
type="card"
:before-leave="onBeforeTabLeave"
>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.code')"
name="code"
data-test-id="code-node-tab-code"
>
<div
ref="codeNodeEditorRef"
:class="['ph-no-capture', 'code-editor-tabs', $style.editorInput]"
/>
<slot name="suffix" />
</el-tab-pane>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.askAi')"
name="ask-ai"
data-test-id="code-node-tab-ai"
>
<!-- Key the AskAI tab to make sure it re-mounts when changing tabs -->
<AskAI
:key="activeTab"
:has-changes="hasChanges"
@replace-code="onReplaceCode"
@started-loading="onAiLoadStart"
@finished-loading="onAiLoadEnd"
/>
</el-tab-pane>
</el-tabs>
<!-- If AskAi not enabled, there's no point in rendering tabs -->
<div v-else :class="$style.fillHeight">
<div ref="codeNodeEditorRef" :class="['ph-no-capture', $style.fillHeight]" />
<slot name="suffix" />
</div>
</div>
</template>
<script setup lang="ts">
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
@@ -411,6 +363,54 @@ function onAiLoadEnd() {
}
</script>
<template>
<div
ref="codeNodeEditorContainerRef"
:class="['code-node-editor', $style['code-node-editor-container'], language]"
@mouseover="onMouseOver"
@mouseout="onMouseOut"
>
<el-tabs
v-if="aiEnabled"
ref="tabs"
v-model="activeTab"
type="card"
:before-leave="onBeforeTabLeave"
>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.code')"
name="code"
data-test-id="code-node-tab-code"
>
<div
ref="codeNodeEditorRef"
:class="['ph-no-capture', 'code-editor-tabs', $style.editorInput]"
/>
<slot name="suffix" />
</el-tab-pane>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.askAi')"
name="ask-ai"
data-test-id="code-node-tab-ai"
>
<!-- Key the AskAI tab to make sure it re-mounts when changing tabs -->
<AskAI
:key="activeTab"
:has-changes="hasChanges"
@replace-code="onReplaceCode"
@started-loading="onAiLoadStart"
@finished-loading="onAiLoadEnd"
/>
</el-tab-pane>
</el-tabs>
<!-- If AskAi not enabled, there's no point in rendering tabs -->
<div v-else :class="$style.fillHeight">
<div ref="codeNodeEditorRef" :class="['ph-no-capture', $style.fillHeight]" />
<slot name="suffix" />
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.el-tabs) {
.code-editor-tabs .cm-editor {

View File

@@ -1,53 +1,3 @@
<template>
<div class="collection-parameter" @keydown.stop>
<div class="collection-parameter-wrapper">
<div v-if="getProperties.length === 0" class="no-items-exist">
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
</div>
<Suspense>
<ParameterInputList
:parameters="getProperties"
:node-values="nodeValues"
:path="path"
:hide-delete="hideDelete"
:indent="true"
:is-read-only="isReadOnly"
@value-changed="valueChanged"
/>
</Suspense>
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
<n8n-button
v-if="(parameter.options ?? []).length === 1"
type="tertiary"
block
:label="getPlaceholderText"
@click="optionSelected((parameter.options ?? [])[0].name)"
/>
<div v-else class="add-option">
<n8n-select
v-model="selectedOption"
:placeholder="getPlaceholderText"
size="small"
filterable
@update:model-value="optionSelected"
>
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="getParameterOptionLabel(item)"
:value="item.name"
data-test-id="collection-parameter-option"
>
</n8n-option>
</n8n-select>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import type { IUpdateInformation } from '@/Interface';
@@ -210,6 +160,56 @@ function valueChanged(parameterData: IUpdateInformation) {
}
</script>
<template>
<div class="collection-parameter" @keydown.stop>
<div class="collection-parameter-wrapper">
<div v-if="getProperties.length === 0" class="no-items-exist">
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
</div>
<Suspense>
<ParameterInputList
:parameters="getProperties"
:node-values="nodeValues"
:path="path"
:hide-delete="hideDelete"
:indent="true"
:is-read-only="isReadOnly"
@value-changed="valueChanged"
/>
</Suspense>
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
<n8n-button
v-if="(parameter.options ?? []).length === 1"
type="tertiary"
block
:label="getPlaceholderText"
@click="optionSelected((parameter.options ?? [])[0].name)"
/>
<div v-else class="add-option">
<n8n-select
v-model="selectedOption"
:placeholder="getPlaceholderText"
size="small"
filterable
@update:model-value="optionSelected"
>
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="getParameterOptionLabel(item)"
:value="item.name"
data-test-id="collection-parameter-option"
>
</n8n-option>
</n8n-select>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
.collection-parameter {
padding-left: var(--spacing-s);

View File

@@ -1,3 +1,10 @@
<script lang="ts" setup>
defineProps<{
loading: boolean;
title?: string;
}>();
</script>
<template>
<n8n-card :class="$style.card" v-bind="$attrs">
<template v-if="!loading && title" #header>
@@ -10,13 +17,6 @@
</n8n-card>
</template>
<script lang="ts" setup>
defineProps<{
loading: boolean;
title?: string;
}>();
</script>
<style lang="scss" module>
.card {
min-width: 235px;

View File

@@ -1,66 +1,3 @@
<template>
<div :class="$style.cardContainer" data-test-id="community-package-card">
<div v-if="loading" :class="$style.cardSkeleton">
<n8n-loading :class="$style.loader" variant="p" :rows="1" />
<n8n-loading :class="$style.loader" variant="p" :rows="1" />
</div>
<div v-else-if="communityPackage" :class="$style.packageCard">
<div :class="$style.cardInfoContainer">
<div :class="$style.cardTitle">
<n8n-text :bold="true" size="large">{{ communityPackage.packageName }}</n8n-text>
</div>
<div :class="$style.cardSubtitle">
<n8n-text :bold="true" size="small" color="text-light">
{{
$locale.baseText('settings.communityNodes.packageNodes.label', {
adjustToNumber: communityPackage.installedNodes.length,
})
}}:&nbsp;
</n8n-text>
<n8n-text size="small" color="text-light">
<span v-for="(node, index) in communityPackage.installedNodes" :key="node.name">
{{ node.name
}}<span v-if="index != communityPackage.installedNodes.length - 1">,</span>
</span>
</n8n-text>
</div>
</div>
<div :class="$style.cardControlsContainer">
<n8n-text :bold="true" size="large" color="text-light">
v{{ communityPackage.installedVersion }}
</n8n-text>
<n8n-tooltip v-if="communityPackage.failedLoading === true" placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.failedToLoad.tooltip') }}
</div>
</template>
<n8n-icon icon="exclamation-triangle" color="danger" size="large" />
</n8n-tooltip>
<n8n-tooltip v-else-if="communityPackage.updateAvailable" placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.updateAvailable.tooltip') }}
</div>
</template>
<n8n-button outline label="Update" @click="onUpdateClick" />
</n8n-tooltip>
<n8n-tooltip v-else placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.upToDate.tooltip') }}
</div>
</template>
<n8n-icon icon="check-circle" color="text-light" size="large" />
</n8n-tooltip>
<div :class="$style.cardActions">
<n8n-action-toggle :actions="packageActions" @action="onAction"></n8n-action-toggle>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { useUIStore } from '@/stores/ui.store';
import type { PublicInstalledPackage } from 'n8n-workflow';
@@ -125,6 +62,69 @@ export default defineComponent({
});
</script>
<template>
<div :class="$style.cardContainer" data-test-id="community-package-card">
<div v-if="loading" :class="$style.cardSkeleton">
<n8n-loading :class="$style.loader" variant="p" :rows="1" />
<n8n-loading :class="$style.loader" variant="p" :rows="1" />
</div>
<div v-else-if="communityPackage" :class="$style.packageCard">
<div :class="$style.cardInfoContainer">
<div :class="$style.cardTitle">
<n8n-text :bold="true" size="large">{{ communityPackage.packageName }}</n8n-text>
</div>
<div :class="$style.cardSubtitle">
<n8n-text :bold="true" size="small" color="text-light">
{{
$locale.baseText('settings.communityNodes.packageNodes.label', {
adjustToNumber: communityPackage.installedNodes.length,
})
}}:&nbsp;
</n8n-text>
<n8n-text size="small" color="text-light">
<span v-for="(node, index) in communityPackage.installedNodes" :key="node.name">
{{ node.name
}}<span v-if="index != communityPackage.installedNodes.length - 1">,</span>
</span>
</n8n-text>
</div>
</div>
<div :class="$style.cardControlsContainer">
<n8n-text :bold="true" size="large" color="text-light">
v{{ communityPackage.installedVersion }}
</n8n-text>
<n8n-tooltip v-if="communityPackage.failedLoading === true" placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.failedToLoad.tooltip') }}
</div>
</template>
<n8n-icon icon="exclamation-triangle" color="danger" size="large" />
</n8n-tooltip>
<n8n-tooltip v-else-if="communityPackage.updateAvailable" placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.updateAvailable.tooltip') }}
</div>
</template>
<n8n-button outline label="Update" @click="onUpdateClick" />
</n8n-tooltip>
<n8n-tooltip v-else placement="top">
<template #content>
<div>
{{ $locale.baseText('settings.communityNodes.upToDate.tooltip') }}
</div>
</template>
<n8n-icon icon="check-circle" color="text-light" size="large" />
</n8n-tooltip>
<div :class="$style.cardActions">
<n8n-action-toggle :actions="packageActions" @action="onAction"></n8n-action-toggle>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.cardContainer {
display: flex;

View File

@@ -1,95 +1,3 @@
<template>
<Modal
width="540px"
:name="COMMUNITY_PACKAGE_INSTALL_MODAL_KEY"
:title="$locale.baseText('settings.communityNodes.installModal.title')"
:event-bus="modalBus"
:center="true"
:before-close="onModalClose"
:show-close="!loading"
>
<template #content>
<div :class="[$style.descriptionContainer, 'p-s']">
<div>
<n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.description') }}
</n8n-text>
{{ ' ' }}
<n8n-link :to="COMMUNITY_NODES_INSTALLATION_DOCS_URL" @click="onMoreInfoTopClick">
{{ $locale.baseText('generic.moreInfo') }}
</n8n-link>
</div>
<n8n-button
:label="$locale.baseText('settings.communityNodes.browseButton.label')"
icon="external-link-alt"
:class="$style.browseButton"
@click="openNPMPage"
/>
</div>
<div :class="[$style.formContainer, 'mt-m']">
<n8n-input-label
:class="$style.labelTooltip"
:label="$locale.baseText('settings.communityNodes.installModal.packageName.label')"
:tooltip-text="
$locale.baseText('settings.communityNodes.installModal.packageName.tooltip', {
interpolate: { npmURL: NPM_KEYWORD_SEARCH_URL },
})
"
>
<n8n-input
v-model="packageName"
name="packageNameInput"
type="text"
:maxlength="214"
:placeholder="
$locale.baseText('settings.communityNodes.installModal.packageName.placeholder')
"
:required="true"
:disabled="loading"
@blur="onInputBlur"
/>
</n8n-input-label>
<div :class="[$style.infoText, 'mt-4xs']">
<span
size="small"
:class="[$style.infoText, infoTextErrorMessage ? $style.error : '']"
v-text="infoTextErrorMessage"
></span>
</div>
<el-checkbox
v-model="userAgreed"
:class="[$style.checkbox, checkboxWarning ? $style.error : '', 'mt-l']"
:disabled="loading"
data-test-id="user-agreement-checkbox"
@update:model-value="onCheckboxChecked"
>
<n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.checkbox.label') }} </n8n-text
><br />
<n8n-link :to="COMMUNITY_NODES_RISKS_DOCS_URL" @click="onLearnMoreLinkClick">{{
$locale.baseText('generic.moreInfo')
}}</n8n-link>
</el-checkbox>
</div>
</template>
<template #footer>
<n8n-button
:loading="loading"
:disabled="!userAgreed || packageName === '' || loading"
:label="
loading
? $locale.baseText('settings.communityNodes.installModal.installButton.label.loading')
: $locale.baseText('settings.communityNodes.installModal.installButton.label')
"
size="large"
float="right"
data-test-id="install-community-package-button"
@click="onInstallClick"
/>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
@@ -189,6 +97,98 @@ export default defineComponent({
});
</script>
<template>
<Modal
width="540px"
:name="COMMUNITY_PACKAGE_INSTALL_MODAL_KEY"
:title="$locale.baseText('settings.communityNodes.installModal.title')"
:event-bus="modalBus"
:center="true"
:before-close="onModalClose"
:show-close="!loading"
>
<template #content>
<div :class="[$style.descriptionContainer, 'p-s']">
<div>
<n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.description') }}
</n8n-text>
{{ ' ' }}
<n8n-link :to="COMMUNITY_NODES_INSTALLATION_DOCS_URL" @click="onMoreInfoTopClick">
{{ $locale.baseText('generic.moreInfo') }}
</n8n-link>
</div>
<n8n-button
:label="$locale.baseText('settings.communityNodes.browseButton.label')"
icon="external-link-alt"
:class="$style.browseButton"
@click="openNPMPage"
/>
</div>
<div :class="[$style.formContainer, 'mt-m']">
<n8n-input-label
:class="$style.labelTooltip"
:label="$locale.baseText('settings.communityNodes.installModal.packageName.label')"
:tooltip-text="
$locale.baseText('settings.communityNodes.installModal.packageName.tooltip', {
interpolate: { npmURL: NPM_KEYWORD_SEARCH_URL },
})
"
>
<n8n-input
v-model="packageName"
name="packageNameInput"
type="text"
:maxlength="214"
:placeholder="
$locale.baseText('settings.communityNodes.installModal.packageName.placeholder')
"
:required="true"
:disabled="loading"
@blur="onInputBlur"
/>
</n8n-input-label>
<div :class="[$style.infoText, 'mt-4xs']">
<span
size="small"
:class="[$style.infoText, infoTextErrorMessage ? $style.error : '']"
v-text="infoTextErrorMessage"
></span>
</div>
<el-checkbox
v-model="userAgreed"
:class="[$style.checkbox, checkboxWarning ? $style.error : '', 'mt-l']"
:disabled="loading"
data-test-id="user-agreement-checkbox"
@update:model-value="onCheckboxChecked"
>
<n8n-text>
{{ $locale.baseText('settings.communityNodes.installModal.checkbox.label') }} </n8n-text
><br />
<n8n-link :to="COMMUNITY_NODES_RISKS_DOCS_URL" @click="onLearnMoreLinkClick">{{
$locale.baseText('generic.moreInfo')
}}</n8n-link>
</el-checkbox>
</div>
</template>
<template #footer>
<n8n-button
:loading="loading"
:disabled="!userAgreed || packageName === '' || loading"
:label="
loading
? $locale.baseText('settings.communityNodes.installModal.installButton.label.loading')
: $locale.baseText('settings.communityNodes.installModal.installButton.label')
"
size="large"
float="right"
data-test-id="install-community-package-button"
@click="onInstallClick"
/>
</template>
</Modal>
</template>
<style module lang="scss">
.descriptionContainer {
display: flex;

View File

@@ -1,37 +1,3 @@
<template>
<Modal
width="540px"
:name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY"
:title="getModalContent.title"
:event-bus="modalBus"
:center="true"
:show-close="!loading"
:before-close="onModalClose"
>
<template #content>
<n8n-text>{{ getModalContent.message }}</n8n-text>
<div
v-if="mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE"
:class="$style.descriptionContainer"
>
<n8n-info-tip theme="info" type="note" :bold="false">
<span v-text="getModalContent.description"></span>
</n8n-info-tip>
</div>
</template>
<template #footer>
<n8n-button
:loading="loading"
:disabled="loading"
:label="loading ? getModalContent.buttonLoadingLabel : getModalContent.buttonLabel"
size="large"
float="right"
@click="onConfirmButtonClick"
/>
</template>
</Modal>
</template>
<script>
import { defineComponent } from 'vue';
import Modal from '@/components/Modal.vue';
@@ -194,6 +160,40 @@ export default defineComponent({
});
</script>
<template>
<Modal
width="540px"
:name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY"
:title="getModalContent.title"
:event-bus="modalBus"
:center="true"
:show-close="!loading"
:before-close="onModalClose"
>
<template #content>
<n8n-text>{{ getModalContent.message }}</n8n-text>
<div
v-if="mode === COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE"
:class="$style.descriptionContainer"
>
<n8n-info-tip theme="info" type="note" :bold="false">
<span v-text="getModalContent.description"></span>
</n8n-info-tip>
</div>
</template>
<template #footer>
<n8n-button
:loading="loading"
:disabled="loading"
:label="loading ? getModalContent.buttonLoadingLabel : getModalContent.buttonLabel"
size="large"
float="right"
@click="onConfirmButtonClick"
/>
</template>
</Modal>
</template>
<style module lang="scss">
.descriptionContainer {
display: flex;

View File

@@ -1,37 +1,3 @@
<template>
<Modal
:name="modalName"
:event-bus="modalBus"
:center="true"
:close-on-press-escape="false"
:before-close="closeDialog"
custom-class="contact-prompt-modal"
width="460px"
>
<template #header>
<n8n-heading tag="h2" size="xlarge" color="text-dark">{{ title }}</n8n-heading>
</template>
<template #content>
<div :class="$style.description">
<n8n-text size="medium" color="text-base">{{ description }}</n8n-text>
</div>
<div @keyup.enter="send">
<n8n-input v-model="email" placeholder="Your email address" />
</div>
<div :class="$style.disclaimer">
<n8n-text size="small" color="text-base"
>David from our product team will get in touch personally</n8n-text
>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button label="Send" float="right" :disabled="!isEmailValid" @click="send" />
</div>
</template>
</Modal>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
@@ -118,6 +84,40 @@ export default defineComponent({
});
</script>
<template>
<Modal
:name="modalName"
:event-bus="modalBus"
:center="true"
:close-on-press-escape="false"
:before-close="closeDialog"
custom-class="contact-prompt-modal"
width="460px"
>
<template #header>
<n8n-heading tag="h2" size="xlarge" color="text-dark">{{ title }}</n8n-heading>
</template>
<template #content>
<div :class="$style.description">
<n8n-text size="medium" color="text-base">{{ description }}</n8n-text>
</div>
<div @keyup.enter="send">
<n8n-input v-model="email" placeholder="Your email address" />
</div>
<div :class="$style.disclaimer">
<n8n-text size="small" color="text-base"
>David from our product team will get in touch personally</n8n-text
>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button label="Send" float="right" :disabled="!isEmailValid" @click="send" />
</div>
</template>
</Modal>
</template>
<style lang="scss" module>
.description {
margin-bottom: var(--spacing-s);

View File

@@ -1,27 +1,3 @@
<template>
<div>
<n8n-input-label :label="label">
<div
:class="{
[$style.copyText]: true,
[$style[size]]: true,
[$style.collapsed]: collapse,
[$style.noHover]: disableCopy,
'ph-no-capture': redactValue,
}"
data-test-id="copy-input"
@click="copy"
>
<span ref="copyInputValue">{{ value }}</span>
<div v-if="!disableCopy" :class="$style.copyButton">
<span>{{ copyButtonText }}</span>
</div>
</div>
</n8n-input-label>
<div v-if="hint" :class="$style.hint">{{ hint }}</div>
</div>
</template>
<script setup lang="ts">
import { useClipboard } from '@/composables/useClipboard';
import { useI18n } from '@/composables/useI18n';
@@ -71,6 +47,30 @@ function copy() {
}
</script>
<template>
<div>
<n8n-input-label :label="label">
<div
:class="{
[$style.copyText]: true,
[$style[size]]: true,
[$style.collapsed]: collapse,
[$style.noHover]: disableCopy,
'ph-no-capture': redactValue,
}"
data-test-id="copy-input"
@click="copy"
>
<span ref="copyInputValue">{{ value }}</span>
<div v-if="!disableCopy" :class="$style.copyButton">
<span>{{ copyButtonText }}</span>
</div>
</div>
</n8n-input-label>
<div v-if="hint" :class="$style.hint">{{ hint }}</div>
</div>
</template>
<style lang="scss" module>
.copyText {
span {

View File

@@ -1,161 +1,3 @@
<template>
<div>
<div :class="$style.config" data-test-id="node-credentials-config-container">
<Banner
v-show="showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
/>
<Banner
v-if="authError && !showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
:details="authError"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
button-loading-label="Retrying"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
@click="$emit('retest')"
/>
<Banner
v-show="showOAuthSuccessBanner && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
:button-title="
$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')
"
data-test-id="oauth-connect-success-banner"
@click="$emit('oauth')"
>
<template v-if="isGoogleOAuthType" #button>
<p
:class="$style.googleReconnectLabel"
v-text="`${$locale.baseText('credentialEdit.credentialConfig.reconnect')}:`"
/>
<GoogleAuthButton @click="$emit('oauth')" />
</template>
</Banner>
<Banner
v-show="testedSuccessfully && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
:button-loading-label="$locale.baseText('credentialEdit.credentialConfig.retrying')"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
data-test-id="credentials-config-container-test-success"
@click="$emit('retest')"
/>
<template v-if="credentialPermissions.update">
<n8n-notice v-if="documentationUrl && credentialProperties.length && !docs" theme="warning">
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
<span class="ml-4xs">
<n8n-link :to="documentationUrl" size="small" bold @click="onDocumentationUrlClick">
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
</n8n-link>
</span>
</n8n-notice>
<AuthTypeSelector
v-if="showAuthTypeSelector && isNewCredential"
:credential-type="credentialType"
@auth-type-changed="onAuthTypeChange"
/>
<CopyInput
v-if="isOAuthType && !allOAuth2BasePropertiesOverridden"
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
:value="oAuthCallbackUrl"
:copy-button-text="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
:hint="
$locale.baseText('credentialEdit.credentialConfig.subtitle', {
interpolate: { appName },
})
"
:toast-title="
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
"
:redact-value="true"
/>
</template>
<EnterpriseEdition v-else :features="[EnterpriseEditionFeature.Sharing]">
<div>
<n8n-info-tip :bold="false">
{{
$locale.baseText('credentialEdit.credentialEdit.info.sharee', {
interpolate: { credentialOwnerName },
})
}}
</n8n-info-tip>
</div>
</EnterpriseEdition>
<CredentialInputs
v-if="credentialType && credentialPermissions.update"
:credential-data="credentialData"
:credential-properties="credentialProperties"
:documentation-url="documentationUrl"
:show-validation-warnings="showValidationWarning"
@update="onDataChange"
/>
<OauthButton
v-if="
isOAuthType &&
requiredPropertiesFilled &&
!isOAuthConnected &&
credentialPermissions.update
"
:is-google-o-auth-type="isGoogleOAuthType"
data-test-id="oauth-connect-button"
@click="$emit('oauth')"
/>
<n8n-text v-if="isMissingCredentials" color="text-base" size="medium">
{{ $locale.baseText('credentialEdit.credentialConfig.missingCredentialType') }}
</n8n-text>
<EnterpriseEdition :features="[EnterpriseEditionFeature.ExternalSecrets]">
<template #fallback>
<n8n-info-tip class="mt-s">
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets') }}
<n8n-link bold :to="$locale.baseText('settings.externalSecrets.docs')" size="small">
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets.moreInfo') }}
</n8n-link>
</n8n-info-tip>
</template>
</EnterpriseEdition>
</div>
<CredentialDocs
v-if="docs"
:credential-type="credentialType"
:documentation-url="documentationUrl"
:docs="docs"
:class="$style.docs"
>
</CredentialDocs>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, watch } from 'vue';
@@ -345,6 +187,164 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
});
</script>
<template>
<div>
<div :class="$style.config" data-test-id="node-credentials-config-container">
<Banner
v-show="showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
/>
<Banner
v-if="authError && !showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
:details="authError"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
button-loading-label="Retrying"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
@click="$emit('retest')"
/>
<Banner
v-show="showOAuthSuccessBanner && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
:button-title="
$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')
"
data-test-id="oauth-connect-success-banner"
@click="$emit('oauth')"
>
<template v-if="isGoogleOAuthType" #button>
<p
:class="$style.googleReconnectLabel"
v-text="`${$locale.baseText('credentialEdit.credentialConfig.reconnect')}:`"
/>
<GoogleAuthButton @click="$emit('oauth')" />
</template>
</Banner>
<Banner
v-show="testedSuccessfully && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
:button-loading-label="$locale.baseText('credentialEdit.credentialConfig.retrying')"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
data-test-id="credentials-config-container-test-success"
@click="$emit('retest')"
/>
<template v-if="credentialPermissions.update">
<n8n-notice v-if="documentationUrl && credentialProperties.length && !docs" theme="warning">
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
<span class="ml-4xs">
<n8n-link :to="documentationUrl" size="small" bold @click="onDocumentationUrlClick">
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
</n8n-link>
</span>
</n8n-notice>
<AuthTypeSelector
v-if="showAuthTypeSelector && isNewCredential"
:credential-type="credentialType"
@auth-type-changed="onAuthTypeChange"
/>
<CopyInput
v-if="isOAuthType && !allOAuth2BasePropertiesOverridden"
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
:value="oAuthCallbackUrl"
:copy-button-text="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
:hint="
$locale.baseText('credentialEdit.credentialConfig.subtitle', {
interpolate: { appName },
})
"
:toast-title="
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
"
:redact-value="true"
/>
</template>
<EnterpriseEdition v-else :features="[EnterpriseEditionFeature.Sharing]">
<div>
<n8n-info-tip :bold="false">
{{
$locale.baseText('credentialEdit.credentialEdit.info.sharee', {
interpolate: { credentialOwnerName },
})
}}
</n8n-info-tip>
</div>
</EnterpriseEdition>
<CredentialInputs
v-if="credentialType && credentialPermissions.update"
:credential-data="credentialData"
:credential-properties="credentialProperties"
:documentation-url="documentationUrl"
:show-validation-warnings="showValidationWarning"
@update="onDataChange"
/>
<OauthButton
v-if="
isOAuthType &&
requiredPropertiesFilled &&
!isOAuthConnected &&
credentialPermissions.update
"
:is-google-o-auth-type="isGoogleOAuthType"
data-test-id="oauth-connect-button"
@click="$emit('oauth')"
/>
<n8n-text v-if="isMissingCredentials" color="text-base" size="medium">
{{ $locale.baseText('credentialEdit.credentialConfig.missingCredentialType') }}
</n8n-text>
<EnterpriseEdition :features="[EnterpriseEditionFeature.ExternalSecrets]">
<template #fallback>
<n8n-info-tip class="mt-s">
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets') }}
<n8n-link bold :to="$locale.baseText('settings.externalSecrets.docs')" size="small">
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets.moreInfo') }}
</n8n-link>
</n8n-info-tip>
</template>
</EnterpriseEdition>
</div>
<CredentialDocs
v-if="docs"
:credential-type="credentialType"
:documentation-url="documentationUrl"
:docs="docs"
:class="$style.docs"
>
</CredentialDocs>
</div>
</template>
<style lang="scss" module>
.config {
--notice-margin: 0;

View File

@@ -1,27 +1,3 @@
<template>
<div :class="$style.docs">
<div :class="$style.header">
<p :class="$style.title">{{ i18n.baseText('credentialEdit.credentialEdit.setupGuide') }}</p>
<n8n-link
:class="$style.docsLink"
theme="text"
new-window
:to="documentationUrl"
@click="onDocumentationUrlClick"
>
{{ i18n.baseText('credentialEdit.credentialEdit.docs') }}
<n8n-icon icon="external-link-alt" size="small" :class="$style.externalIcon" />
</n8n-link>
</div>
<VueMarkdown :source="docs" :options="{ html: true }" :class="$style.markdown" />
<Feedback
:class="$style.feedback"
:model-value="submittedFeedback"
@update:model-value="onFeedback"
/>
</div>
</template>
<script setup lang="ts">
import Feedback from '@/components/Feedback.vue';
import { useI18n } from '@/composables/useI18n';
@@ -66,6 +42,30 @@ function onDocumentationUrlClick(): void {
}
</script>
<template>
<div :class="$style.docs">
<div :class="$style.header">
<p :class="$style.title">{{ i18n.baseText('credentialEdit.credentialEdit.setupGuide') }}</p>
<n8n-link
:class="$style.docsLink"
theme="text"
new-window
:to="documentationUrl"
@click="onDocumentationUrlClick"
>
{{ i18n.baseText('credentialEdit.credentialEdit.docs') }}
<n8n-icon icon="external-link-alt" size="small" :class="$style.externalIcon" />
</n8n-link>
</div>
<VueMarkdown :source="docs" :options="{ html: true }" :class="$style.markdown" />
<Feedback
:class="$style.feedback"
:model-value="submittedFeedback"
@update:model-value="onFeedback"
/>
</div>
</template>
<style lang="scss" module>
.docs {
background-color: var(--color-background-light);

View File

@@ -1,118 +1,3 @@
<template>
<Modal
:name="modalName"
:custom-class="$style.credentialModal"
:event-bus="modalBus"
:loading="loading"
:before-close="beforeClose"
width="70%"
height="80%"
>
<template #header>
<div :class="$style.header">
<div :class="$style.credInfo">
<div :class="$style.credIcon">
<CredentialIcon :credential-type-name="defaultCredentialTypeName" />
</div>
<InlineNameEdit
:model-value="credentialName"
:subtitle="credentialType ? credentialType.displayName : ''"
:readonly="!credentialPermissions.update || !credentialType"
type="Credential"
data-test-id="credential-name"
@update:model-value="onNameEdit"
/>
</div>
<div :class="$style.credActions">
<n8n-icon-button
v-if="currentCredential && credentialPermissions.delete"
:title="$locale.baseText('credentialEdit.credentialEdit.delete')"
icon="trash"
type="tertiary"
:disabled="isSaving"
:loading="isDeleting"
data-test-id="credential-delete-button"
@click="deleteCredential"
/>
<SaveButton
v-if="showSaveButton"
:saved="!hasUnsavedChanges && !isTesting"
:is-saving="isSaving || isTesting"
:saving-label="
isTesting
? $locale.baseText('credentialEdit.credentialEdit.testing')
: $locale.baseText('credentialEdit.credentialEdit.saving')
"
data-test-id="credential-save-button"
@click="saveCredential"
/>
</div>
</div>
</template>
<template #content>
<div :class="$style.container" data-test-id="credential-edit-dialog">
<div :class="$style.sidebar">
<n8n-menu
mode="tabs"
:items="sidebarItems"
:transparent-background="true"
@select="onTabSelect"
></n8n-menu>
</div>
<div
v-if="activeTab === 'connection' && credentialType"
ref="contentRef"
:class="$style.mainContent"
>
<CredentialConfig
:credential-type="credentialType"
:credential-properties="credentialProperties"
:credential-data="credentialData"
:credential-id="credentialId"
:show-validation-warning="showValidationWarning"
:auth-error="authError"
:tested-successfully="testedSuccessfully"
:is-o-auth-type="isOAuthType"
:is-o-auth-connected="isOAuthConnected"
:is-retesting="isRetesting"
:parent-types="parentTypes"
:required-properties-filled="requiredPropertiesFilled"
:credential-permissions="credentialPermissions"
:all-o-auth2-base-properties-overridden="allOAuth2BasePropertiesOverridden"
:mode="mode"
:selected-credential="selectedCredential"
:show-auth-type-selector="requiredCredentials"
@update="onDataChange"
@oauth="oAuthCredentialAuthorize"
@retest="retestCredential"
@scroll-to-top="scrollToTop"
@auth-type-changed="onAuthTypeChanged"
/>
</div>
<div v-else-if="showSharingContent" :class="$style.mainContent">
<CredentialSharing
:credential="currentCredential"
:credential-data="credentialData"
:credential-id="credentialId"
:credential-permissions="credentialPermissions"
:modal-bus="modalBus"
@update:model-value="onChangeSharedWith"
/>
</div>
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
<CredentialInfo
:current-credential="currentCredential"
:credential-permissions="credentialPermissions"
/>
</div>
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
@@ -1143,6 +1028,121 @@ function resetCredentialData(): void {
}
</script>
<template>
<Modal
:name="modalName"
:custom-class="$style.credentialModal"
:event-bus="modalBus"
:loading="loading"
:before-close="beforeClose"
width="70%"
height="80%"
>
<template #header>
<div :class="$style.header">
<div :class="$style.credInfo">
<div :class="$style.credIcon">
<CredentialIcon :credential-type-name="defaultCredentialTypeName" />
</div>
<InlineNameEdit
:model-value="credentialName"
:subtitle="credentialType ? credentialType.displayName : ''"
:readonly="!credentialPermissions.update || !credentialType"
type="Credential"
data-test-id="credential-name"
@update:model-value="onNameEdit"
/>
</div>
<div :class="$style.credActions">
<n8n-icon-button
v-if="currentCredential && credentialPermissions.delete"
:title="$locale.baseText('credentialEdit.credentialEdit.delete')"
icon="trash"
type="tertiary"
:disabled="isSaving"
:loading="isDeleting"
data-test-id="credential-delete-button"
@click="deleteCredential"
/>
<SaveButton
v-if="showSaveButton"
:saved="!hasUnsavedChanges && !isTesting"
:is-saving="isSaving || isTesting"
:saving-label="
isTesting
? $locale.baseText('credentialEdit.credentialEdit.testing')
: $locale.baseText('credentialEdit.credentialEdit.saving')
"
data-test-id="credential-save-button"
@click="saveCredential"
/>
</div>
</div>
</template>
<template #content>
<div :class="$style.container" data-test-id="credential-edit-dialog">
<div :class="$style.sidebar">
<n8n-menu
mode="tabs"
:items="sidebarItems"
:transparent-background="true"
@select="onTabSelect"
></n8n-menu>
</div>
<div
v-if="activeTab === 'connection' && credentialType"
ref="contentRef"
:class="$style.mainContent"
>
<CredentialConfig
:credential-type="credentialType"
:credential-properties="credentialProperties"
:credential-data="credentialData"
:credential-id="credentialId"
:show-validation-warning="showValidationWarning"
:auth-error="authError"
:tested-successfully="testedSuccessfully"
:is-o-auth-type="isOAuthType"
:is-o-auth-connected="isOAuthConnected"
:is-retesting="isRetesting"
:parent-types="parentTypes"
:required-properties-filled="requiredPropertiesFilled"
:credential-permissions="credentialPermissions"
:all-o-auth2-base-properties-overridden="allOAuth2BasePropertiesOverridden"
:mode="mode"
:selected-credential="selectedCredential"
:show-auth-type-selector="requiredCredentials"
@update="onDataChange"
@oauth="oAuthCredentialAuthorize"
@retest="retestCredential"
@scroll-to-top="scrollToTop"
@auth-type-changed="onAuthTypeChanged"
/>
</div>
<div v-else-if="showSharingContent" :class="$style.mainContent">
<CredentialSharing
:credential="currentCredential"
:credential-data="credentialData"
:credential-id="credentialId"
:credential-permissions="credentialPermissions"
:modal-bus="modalBus"
@update:model-value="onChangeSharedWith"
/>
</div>
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
<CredentialInfo
:current-credential="currentCredential"
:credential-permissions="credentialPermissions"
/>
</div>
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>
</div>
</div>
</template>
</Modal>
</template>
<style module lang="scss">
.credentialModal {
--dialog-max-width: 1200px;

View File

@@ -1,3 +1,23 @@
<script lang="ts">
import { defineComponent } from 'vue';
import TimeAgo from '../TimeAgo.vue';
import type { INodeTypeDescription } from 'n8n-workflow';
export default defineComponent({
name: 'CredentialInfo',
components: {
TimeAgo,
},
props: ['currentCredential', 'credentialPermissions'],
methods: {
shortNodeType(nodeType: INodeTypeDescription) {
return this.$locale.shortNodeType(nodeType.name);
},
},
});
</script>
<template>
<div :class="$style.container">
<el-row v-if="currentCredential">
@@ -37,26 +57,6 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TimeAgo from '../TimeAgo.vue';
import type { INodeTypeDescription } from 'n8n-workflow';
export default defineComponent({
name: 'CredentialInfo',
components: {
TimeAgo,
},
props: ['currentCredential', 'credentialPermissions'],
methods: {
shortNodeType(nodeType: INodeTypeDescription) {
return this.$locale.shortNodeType(nodeType.name);
},
},
});
</script>
<style lang="scss" module>
.container {
> * {

View File

@@ -1,28 +1,3 @@
<template>
<div v-if="credentialProperties.length" :class="$style.container" @keydown.stop>
<form
v-for="parameter in credentialProperties"
:key="parameter.name"
autocomplete="off"
data-test-id="credential-connection-parameter"
@submit.prevent
>
<!-- Why form? to break up inputs, to prevent Chrome autofill -->
<n8n-notice v-if="parameter.type === 'notice'" :content="parameter.displayName" />
<ParameterInputExpanded
v-else
:parameter="parameter"
:value="credentialDataValues[parameter.name]"
:documentation-url="documentationUrl"
:show-validation-warnings="showValidationWarnings"
:label="{ size: 'medium' }"
event-source="credentials"
@update="valueChanged"
/>
</form>
</div>
</template>
<script setup lang="ts">
import type {
ICredentialDataDecryptedObject,
@@ -60,6 +35,31 @@ function valueChanged(parameterData: IUpdateInformation) {
}
</script>
<template>
<div v-if="credentialProperties.length" :class="$style.container" @keydown.stop>
<form
v-for="parameter in credentialProperties"
:key="parameter.name"
autocomplete="off"
data-test-id="credential-connection-parameter"
@submit.prevent
>
<!-- Why form? to break up inputs, to prevent Chrome autofill -->
<n8n-notice v-if="parameter.type === 'notice'" :content="parameter.displayName" />
<ParameterInputExpanded
v-else
:parameter="parameter"
:value="credentialDataValues[parameter.name]"
:documentation-url="documentationUrl"
:show-validation-warnings="showValidationWarnings"
:label="{ size: 'medium' }"
event-source="credentials"
@update="valueChanged"
/>
</form>
</div>
</template>
<style lang="scss" module>
.container {
> * {

View File

@@ -1,52 +1,3 @@
<template>
<div :class="$style.container">
<div v-if="!isSharingEnabled">
<N8nActionBox
:heading="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
)
"
:description="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description,
)
"
:button-text="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button,
)
"
@click:button="goToUpgrade"
/>
</div>
<div v-else>
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</N8nInfoTip>
<N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
</N8nInfoTip>
<N8nInfoTip v-else :bold="false" class="mb-s">
{{
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
interpolate: { credentialOwnerName },
})
}}
</N8nInfoTip>
<ProjectSharing
v-model="sharedWithProjects"
:projects="projects"
:roles="credentialRoles"
:home-project="homeProject"
:readonly="!credentialPermissions.share"
:static="!credentialPermissions.share"
:placeholder="sharingSelectPlaceholder"
/>
</div>
</div>
</template>
<script lang="ts">
import type {
ICredentialsResponse,
@@ -208,6 +159,55 @@ export default defineComponent({
});
</script>
<template>
<div :class="$style.container">
<div v-if="!isSharingEnabled">
<N8nActionBox
:heading="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
)
"
:description="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description,
)
"
:button-text="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button,
)
"
@click:button="goToUpgrade"
/>
</div>
<div v-else>
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</N8nInfoTip>
<N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
</N8nInfoTip>
<N8nInfoTip v-else :bold="false" class="mb-s">
{{
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
interpolate: { credentialOwnerName },
})
}}
</N8nInfoTip>
<ProjectSharing
v-model="sharedWithProjects"
:projects="projects"
:roles="credentialRoles"
:home-project="homeProject"
:readonly="!credentialPermissions.share"
:static="!credentialPermissions.share"
:placeholder="sharingSelectPlaceholder"
/>
</div>
</div>
</template>
<style lang="scss" module>
.container {
width: 100%;

View File

@@ -1,11 +1,3 @@
<template>
<button
:class="$style.googleAuthBtn"
:title="$locale.baseText('credentialEdit.oAuthButton.signInWithGoogle')"
:style="googleAuthButtons"
/>
</template>
<script lang="ts" setup>
import { useUIStore } from '@/stores/ui.store';
import { useRootStore } from '@/stores/root.store';
@@ -20,6 +12,14 @@ const googleAuthButtons = {
};
</script>
<template>
<button
:class="$style.googleAuthBtn"
:title="$locale.baseText('credentialEdit.oAuthButton.signInWithGoogle')"
:style="googleAuthButtons"
/>
</template>
<style module lang="scss">
.googleAuthBtn {
--google-auth-btn-height: 46px;

View File

@@ -1,3 +1,11 @@
<script lang="ts" setup>
import GoogleAuthButton from './GoogleAuthButton.vue';
defineProps<{
isGoogleOAuthType: boolean;
}>();
</script>
<template>
<div :class="$style.container">
<GoogleAuthButton v-if="isGoogleOAuthType" />
@@ -9,14 +17,6 @@
</div>
</template>
<script lang="ts" setup>
import GoogleAuthButton from './GoogleAuthButton.vue';
defineProps<{
isGoogleOAuthType: boolean;
}>();
</script>
<style module lang="scss">
.container {
display: inline-block;

View File

@@ -1,53 +1,3 @@
<template>
<div>
<div :class="$style['parameter-value-container']">
<n8n-select
ref="innerSelect"
:size="inputSize"
filterable
:model-value="displayValue"
:placeholder="$locale.baseText('parameterInput.select')"
:title="displayTitle"
:disabled="isReadOnly"
data-test-id="credential-select"
@update:model-value="(value: string) => $emit('update:modelValue', value)"
@keydown.stop
@focus="$emit('setFocus')"
@blur="$emit('onBlur')"
>
<n8n-option
v-for="credType in supportedCredentialTypes"
:key="credType.name"
:value="credType.name"
:label="credType.displayName"
data-test-id="credential-select-option"
>
<div class="list-option">
<div class="option-headline">
{{ credType.displayName }}
</div>
</div>
</n8n-option>
</n8n-select>
<slot name="issues-and-options" />
</div>
<ScopesNotice
v-if="scopes.length > 0"
:active-credential-type="activeCredentialType"
:scopes="scopes"
/>
<div>
<NodeCredentials
:node="node"
:readonly="isReadOnly"
:override-cred-type="node.parameters[parameter.name]"
@credential-selected="(updateInformation) => $emit('credentialSelected', updateInformation)"
/>
</div>
</div>
</template>
<script lang="ts">
import type { ICredentialType } from 'n8n-workflow';
import { defineComponent } from 'vue';
@@ -156,6 +106,56 @@ export default defineComponent({
});
</script>
<template>
<div>
<div :class="$style['parameter-value-container']">
<n8n-select
ref="innerSelect"
:size="inputSize"
filterable
:model-value="displayValue"
:placeholder="$locale.baseText('parameterInput.select')"
:title="displayTitle"
:disabled="isReadOnly"
data-test-id="credential-select"
@update:model-value="(value: string) => $emit('update:modelValue', value)"
@keydown.stop
@focus="$emit('setFocus')"
@blur="$emit('onBlur')"
>
<n8n-option
v-for="credType in supportedCredentialTypes"
:key="credType.name"
:value="credType.name"
:label="credType.displayName"
data-test-id="credential-select-option"
>
<div class="list-option">
<div class="option-headline">
{{ credType.displayName }}
</div>
</div>
</n8n-option>
</n8n-select>
<slot name="issues-and-options" />
</div>
<ScopesNotice
v-if="scopes.length > 0"
:active-credential-type="activeCredentialType"
:scopes="scopes"
/>
<div>
<NodeCredentials
:node="node"
:readonly="isReadOnly"
:override-cred-type="node.parameters[parameter.name]"
@credential-selected="(updateInformation) => $emit('credentialSelected', updateInformation)"
/>
</div>
</div>
</template>
<style module lang="scss">
.parameter-value-container {
display: flex;

View File

@@ -1,62 +1,3 @@
<template>
<Modal
:name="CREDENTIAL_SELECT_MODAL_KEY"
:event-bus="modalBus"
width="50%"
:center="true"
:loading="loading"
max-width="460px"
min-height="250px"
>
<template #header>
<h2 :class="$style.title">
{{ $locale.baseText('credentialSelectModal.addNewCredential') }}
</h2>
</template>
<template #content>
<div>
<div :class="$style.subtitle">
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
</div>
<n8n-select
ref="select"
filterable
default-first-option
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
size="xlarge"
:model-value="selected"
data-test-id="new-credential-type-select"
@update:model-value="onSelect"
>
<template #prefix>
<font-awesome-icon icon="search" />
</template>
<n8n-option
v-for="credential in credentialsStore.allCredentialTypes"
:key="credential.name"
:value="credential.name"
:label="credential.displayName"
filterable
data-test-id="new-credential-type-select-option"
/>
</n8n-select>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button
:label="$locale.baseText('credentialSelectModal.continue')"
float="right"
size="large"
:disabled="!selected"
data-test-id="new-credential-type-button"
@click="openCredentialType"
/>
</div>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Modal from './Modal.vue';
@@ -125,6 +66,65 @@ export default defineComponent({
});
</script>
<template>
<Modal
:name="CREDENTIAL_SELECT_MODAL_KEY"
:event-bus="modalBus"
width="50%"
:center="true"
:loading="loading"
max-width="460px"
min-height="250px"
>
<template #header>
<h2 :class="$style.title">
{{ $locale.baseText('credentialSelectModal.addNewCredential') }}
</h2>
</template>
<template #content>
<div>
<div :class="$style.subtitle">
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
</div>
<n8n-select
ref="select"
filterable
default-first-option
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
size="xlarge"
:model-value="selected"
data-test-id="new-credential-type-select"
@update:model-value="onSelect"
>
<template #prefix>
<font-awesome-icon icon="search" />
</template>
<n8n-option
v-for="credential in credentialsStore.allCredentialTypes"
:key="credential.name"
:value="credential.name"
:label="credential.displayName"
filterable
data-test-id="new-credential-type-select-option"
/>
</n8n-select>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button
:label="$locale.baseText('credentialSelectModal.continue')"
float="right"
size="large"
:disabled="!selected"
data-test-id="new-credential-type-button"
@click="openCredentialType"
/>
</div>
</template>
</Modal>
</template>
<style module lang="scss">
.title {
font-size: var(--font-size-xl);

View File

@@ -1,20 +1,3 @@
<template>
<component
:is="tag"
ref="wrapper"
:class="{ [$style.dragging]: isDragging }"
@mousedown="onDragStart"
>
<slot :is-dragging="isDragging"></slot>
<Teleport to="body">
<div v-show="isDragging" ref="draggable" :class="$style.draggable" :style="draggableStyle">
<slot name="preview" :can-drop="canDrop" :el="draggingElement"></slot>
</div>
</Teleport>
</component>
</template>
<script setup lang="ts">
import type { XYPosition } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
@@ -137,6 +120,23 @@ const onDragEnd = () => {
};
</script>
<template>
<component
:is="tag"
ref="wrapper"
:class="{ [$style.dragging]: isDragging }"
@mousedown="onDragStart"
>
<slot :is-dragging="isDragging"></slot>
<Teleport to="body">
<div v-show="isDragging" ref="draggable" :class="$style.draggable" :style="draggableStyle">
<slot name="preview" :can-drop="canDrop" :el="draggingElement"></slot>
</div>
</Teleport>
</component>
</template>
<style lang="scss" module>
.dragging {
visibility: visible;

View File

@@ -1,9 +1,3 @@
<template>
<div ref="targetRef" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mouseup="onMouseUp">
<slot :droppable="droppable" :active-drop="activeDrop"></slot>
</div>
</template>
<script setup lang="ts">
import type { XYPosition } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
@@ -84,3 +78,9 @@ function getStickyPosition(): XYPosition | null {
return [left + props.stickyOffset[0], top + props.stickyOffset[1]];
}
</script>
<template>
<div ref="targetRef" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mouseup="onMouseUp">
<slot :droppable="droppable" :active-drop="activeDrop"></slot>
</div>
</template>

View File

@@ -1,52 +1,3 @@
<template>
<Modal
:name="modalName"
:event-bus="modalBus"
:title="$locale.baseText('duplicateWorkflowDialog.duplicateWorkflow')"
:center="true"
width="420px"
@enter="save"
>
<template #content>
<div :class="$style.content">
<n8n-input
ref="nameInput"
v-model="name"
:placeholder="$locale.baseText('duplicateWorkflowDialog.enterWorkflowName')"
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
/>
<TagsDropdown
v-if="settingsStore.areTagsEnabled"
ref="dropdown"
v-model="currentTagIds"
:create-enabled="true"
:event-bus="dropdownBus"
:placeholder="$locale.baseText('duplicateWorkflowDialog.chooseOrCreateATag')"
@blur="onTagsBlur"
@esc="onTagsEsc"
/>
</div>
</template>
<template #footer="{ close }">
<div :class="$style.footer">
<n8n-button
:loading="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.save')"
float="right"
@click="save"
/>
<n8n-button
type="secondary"
:disabled="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.cancel')"
float="right"
@click="close"
/>
</div>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
@@ -199,6 +150,55 @@ export default defineComponent({
});
</script>
<template>
<Modal
:name="modalName"
:event-bus="modalBus"
:title="$locale.baseText('duplicateWorkflowDialog.duplicateWorkflow')"
:center="true"
width="420px"
@enter="save"
>
<template #content>
<div :class="$style.content">
<n8n-input
ref="nameInput"
v-model="name"
:placeholder="$locale.baseText('duplicateWorkflowDialog.enterWorkflowName')"
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
/>
<TagsDropdown
v-if="settingsStore.areTagsEnabled"
ref="dropdown"
v-model="currentTagIds"
:create-enabled="true"
:event-bus="dropdownBus"
:placeholder="$locale.baseText('duplicateWorkflowDialog.chooseOrCreateATag')"
@blur="onTagsBlur"
@esc="onTagsEsc"
/>
</div>
</template>
<template #footer="{ close }">
<div :class="$style.footer">
<n8n-button
:loading="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.save')"
float="right"
@click="save"
/>
<n8n-button
type="secondary"
:disabled="isSaving"
:label="$locale.baseText('duplicateWorkflowDialog.cancel')"
float="right"
@click="close"
/>
</div>
</template>
</Modal>
</template>
<style lang="scss" module>
.content {
> *:not(:last-child) {

View File

@@ -1,10 +1,3 @@
<template>
<div>
<slot v-if="canAccess" />
<slot v-else name="fallback" />
</div>
</template>
<script lang="ts">
import { type PropType, defineComponent } from 'vue';
import type { EnterpriseEditionFeatureValue } from '@/Interface';
@@ -29,3 +22,10 @@ export default defineComponent({
},
});
</script>
<template>
<div>
<slot v-if="canAccess" />
<slot v-else name="fallback" />
</div>
</template>

View File

@@ -1,10 +1,3 @@
<template>
<!-- mock el-input element to apply styles -->
<div :class="{ 'el-input': true, 'static-size': staticSize }" :data-value="hiddenValue">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
@@ -26,6 +19,13 @@ const hiddenValue = computed(() => {
});
</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>
$--horiz-padding: 15px;

View File

@@ -1,19 +1,3 @@
<template>
<ExpandableInputBase :model-value="modelValue" :placeholder="placeholder">
<input
ref="inputRef"
class="el-input__inner"
:value="modelValue"
:placeholder="placeholder"
:maxlength="maxlength"
size="4"
@input="onInput"
@keydown.enter="onEnter"
@keydown.esc="onEscape"
/>
</ExpandableInputBase>
</template>
<script setup lang="ts">
import type { EventBus } from 'n8n-design-system';
import { onBeforeUnmount, onMounted, ref } from 'vue';
@@ -78,3 +62,19 @@ function onEscape() {
emit('esc');
}
</script>
<template>
<ExpandableInputBase :model-value="modelValue" :placeholder="placeholder">
<input
ref="inputRef"
class="el-input__inner"
:value="modelValue"
:placeholder="placeholder"
:maxlength="maxlength"
size="4"
@input="onInput"
@keydown.enter="onEnter"
@keydown.esc="onEscape"
/>
</ExpandableInputBase>
</template>

View File

@@ -1,3 +1,13 @@
<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
@@ -9,16 +19,6 @@
</ExpandableInputBase>
</template>
<script setup lang="ts">
import ExpandableInputBase from './ExpandableInputBase.vue';
type Props = {
modelValue: string;
};
defineProps<Props>();
</script>
<style lang="scss" scoped>
input,
input:hover {

View File

@@ -1,94 +1,3 @@
<template>
<el-dialog
width="calc(100vw - var(--spacing-3xl))"
append-to-body
:class="$style.modal"
:model-value="dialogVisible"
:before-close="closeDialog"
>
<button :class="$style.close" @click="closeDialog">
<Close height="18" width="18" />
</button>
<div :class="$style.container">
<div :class="$style.sidebar">
<N8nInput
v-model="search"
size="small"
:class="$style.search"
:placeholder="i18n.baseText('ndv.search.placeholder.input.schema')"
>
<template #prefix>
<N8nIcon :class="$style.ioSearchIcon" icon="search" />
</template>
</N8nInput>
<RunDataSchema
:class="$style.schema"
:search="appliedSearch"
:nodes="parentNodes"
mapping-enabled
pane-type="input"
connection-type="main"
/>
</div>
<div :class="$style.io">
<div :class="$style.input">
<div :class="$style.header">
<N8nText bold size="large">
{{ i18n.baseText('expressionEdit.expression') }}
</N8nText>
<N8nText
:class="$style.tip"
size="small"
v-html="i18n.baseText('expressionTip.javascript')"
/>
</div>
<DraggableTarget :class="$style.editorContainer" type="mapping" @drop="onDrop">
<template #default>
<ExpressionEditorModalInput
ref="expressionInputRef"
:model-value="modelValue"
:is-read-only="isReadOnly"
:path="path"
:class="[
$style.editor,
{
'ph-no-capture': redactValues,
},
]"
data-test-id="expression-modal-input"
@change="valueChanged"
@close="closeDialog"
/>
</template>
</DraggableTarget>
</div>
<div :class="$style.output">
<div :class="$style.header">
<N8nText bold size="large">
{{ i18n.baseText('parameterInput.result') }}
</N8nText>
<OutputItemSelect />
</div>
<div :class="[$style.editorContainer, { 'ph-no-capture': redactValues }]">
<ExpressionOutput
ref="expressionResultRef"
:class="$style.editor"
:segments="segments"
:extensions="theme"
data-test-id="expression-modal-output"
/>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/ExpressionEditorModalInput.vue';
import { computed, ref, toRaw, watch } from 'vue';
@@ -211,6 +120,97 @@ async function onDrop(expression: string, event: MouseEvent) {
}
</script>
<template>
<el-dialog
width="calc(100vw - var(--spacing-3xl))"
append-to-body
:class="$style.modal"
:model-value="dialogVisible"
:before-close="closeDialog"
>
<button :class="$style.close" @click="closeDialog">
<Close height="18" width="18" />
</button>
<div :class="$style.container">
<div :class="$style.sidebar">
<N8nInput
v-model="search"
size="small"
:class="$style.search"
:placeholder="i18n.baseText('ndv.search.placeholder.input.schema')"
>
<template #prefix>
<N8nIcon :class="$style.ioSearchIcon" icon="search" />
</template>
</N8nInput>
<RunDataSchema
:class="$style.schema"
:search="appliedSearch"
:nodes="parentNodes"
mapping-enabled
pane-type="input"
connection-type="main"
/>
</div>
<div :class="$style.io">
<div :class="$style.input">
<div :class="$style.header">
<N8nText bold size="large">
{{ i18n.baseText('expressionEdit.expression') }}
</N8nText>
<N8nText
:class="$style.tip"
size="small"
v-html="i18n.baseText('expressionTip.javascript')"
/>
</div>
<DraggableTarget :class="$style.editorContainer" type="mapping" @drop="onDrop">
<template #default>
<ExpressionEditorModalInput
ref="expressionInputRef"
:model-value="modelValue"
:is-read-only="isReadOnly"
:path="path"
:class="[
$style.editor,
{
'ph-no-capture': redactValues,
},
]"
data-test-id="expression-modal-input"
@change="valueChanged"
@close="closeDialog"
/>
</template>
</DraggableTarget>
</div>
<div :class="$style.output">
<div :class="$style.header">
<N8nText bold size="large">
{{ i18n.baseText('parameterInput.result') }}
</N8nText>
<OutputItemSelect />
</div>
<div :class="[$style.editorContainer, { 'ph-no-capture': redactValues }]">
<ExpressionOutput
ref="expressionResultRef"
:class="$style.editor"
:segments="segments"
:extensions="theme"
data-test-id="expression-modal-output"
/>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<style module lang="scss">
.modal {
--dialog-close-top: var(--spacing-m);

View File

@@ -1,7 +1,3 @@
<template>
<div ref="root" :class="$style.editor" @keydown.stop></div>
</template>
<script setup lang="ts">
import { history } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
@@ -109,6 +105,10 @@ onMounted(() => {
defineExpose({ editor });
</script>
<template>
<div ref="root" :class="$style.editor" @keydown.stop></div>
</template>
<style lang="scss" module>
:global(.cm-content) {
border-radius: var(--border-radius-base);

View File

@@ -1,3 +1,9 @@
<script lang="ts">
export default {
name: 'ExpressionFunctionIcon',
};
</script>
<template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -7,10 +13,4 @@
</svg>
</template>
<script lang="ts">
export default {
name: 'ExpressionFunctionIcon',
};
</script>
<style lang="scss"></style>

View File

@@ -1,31 +1,3 @@
<template>
<div v-if="featureInfo" :class="[$style.container]">
<div v-if="showTitle" class="mb-2xl">
<n8n-heading size="2xlarge">
{{ $locale.baseText(featureInfo.featureName) }}
</n8n-heading>
</div>
<div v-if="featureInfo.infoText" class="mb-l">
<n8n-info-tip theme="info" type="note">
<span v-html="$locale.baseText(featureInfo.infoText)"></span>
</n8n-info-tip>
</div>
<div :class="$style.actionBoxContainer">
<n8n-action-box
:description="$locale.baseText(featureInfo.actionBoxDescription)"
:button-text="
$locale.baseText(featureInfo.actionBoxButtonLabel || 'fakeDoor.actionBox.button.label')
"
@click:button="openLinkPage"
>
<template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template>
</n8n-action-box>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
@@ -75,6 +47,34 @@ export default defineComponent({
});
</script>
<template>
<div v-if="featureInfo" :class="[$style.container]">
<div v-if="showTitle" class="mb-2xl">
<n8n-heading size="2xlarge">
{{ $locale.baseText(featureInfo.featureName) }}
</n8n-heading>
</div>
<div v-if="featureInfo.infoText" class="mb-l">
<n8n-info-tip theme="info" type="note">
<span v-html="$locale.baseText(featureInfo.infoText)"></span>
</n8n-info-tip>
</div>
<div :class="$style.actionBoxContainer">
<n8n-action-box
:description="$locale.baseText(featureInfo.actionBoxDescription)"
:button-text="
$locale.baseText(featureInfo.actionBoxButtonLabel || 'fakeDoor.actionBox.button.label')
"
@click:button="openLinkPage"
>
<template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template>
</n8n-action-box>
</div>
</div>
</template>
<style lang="scss" module>
.actionBoxContainer {
text-align: center;

View File

@@ -1,131 +1,3 @@
<template>
<div
class="fixed-collection-parameter"
:data-test-id="`fixed-collection-${parameter.name}`"
@keydown.stop
>
<div v-if="getProperties.length === 0" class="no-items-exist">
<n8n-text size="small">{{
$locale.baseText('fixedCollectionParameter.currentlyNoItemsExist')
}}</n8n-text>
</div>
<div
v-for="property in getProperties"
:key="property.name"
class="fixed-collection-parameter-property"
>
<n8n-input-label
v-if="property.displayName !== '' && parameter.options && parameter.options.length !== 1"
:label="$locale.nodeText().inputLabelDisplayName(property, path)"
:underline="true"
size="small"
color="text-dark"
/>
<div v-if="multipleValues">
<div
v-for="(_, index) in mutableValues[property.name]"
:key="property.name + index"
class="parameter-item"
>
<div
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
>
<div v-if="!isReadOnly" class="delete-option">
<n8n-icon-button
type="tertiary"
text
size="mini"
icon="trash"
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
@click="deleteOption(property.name, index)"
></n8n-icon-button>
<n8n-icon-button
v-if="sortable && index !== 0"
type="tertiary"
text
size="mini"
icon="angle-up"
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
@click="moveOptionUp(property.name, index)"
></n8n-icon-button>
<n8n-icon-button
v-if="sortable && index !== mutableValues[property.name].length - 1"
type="tertiary"
text
size="mini"
icon="angle-down"
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
@click="moveOptionDown(property.name, index)"
></n8n-icon-button>
</div>
<Suspense>
<ParameterInputList
:parameters="property.values"
:node-values="nodeValues"
:path="getPropertyPath(property.name, index)"
:hide-delete="true"
:is-read-only="isReadOnly"
@value-changed="valueChanged"
/>
</Suspense>
</div>
</div>
</div>
<div v-else class="parameter-item">
<div class="parameter-item-wrapper">
<div v-if="!isReadOnly" class="delete-option">
<n8n-icon-button
type="tertiary"
text
size="mini"
icon="trash"
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
@click="deleteOption(property.name)"
></n8n-icon-button>
</div>
<ParameterInputList
:parameters="property.values"
:node-values="nodeValues"
:path="getPropertyPath(property.name)"
:is-read-only="isReadOnly"
class="parameter-item"
:hide-delete="true"
@value-changed="valueChanged"
/>
</div>
</div>
</div>
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="controls">
<n8n-button
v-if="parameter.options && parameter.options.length === 1"
type="tertiary"
block
data-test-id="fixed-collection-add"
:label="getPlaceholderText"
@click="optionSelected(parameter.options[0].name)"
/>
<div v-else class="add-option">
<n8n-select
v-model="selectedOption"
:placeholder="getPlaceholderText"
size="small"
filterable
@update:model-value="optionSelected"
>
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
:value="item.name"
></n8n-option>
</n8n-select>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
@@ -339,6 +211,134 @@ export default defineComponent({
});
</script>
<template>
<div
class="fixed-collection-parameter"
:data-test-id="`fixed-collection-${parameter.name}`"
@keydown.stop
>
<div v-if="getProperties.length === 0" class="no-items-exist">
<n8n-text size="small">{{
$locale.baseText('fixedCollectionParameter.currentlyNoItemsExist')
}}</n8n-text>
</div>
<div
v-for="property in getProperties"
:key="property.name"
class="fixed-collection-parameter-property"
>
<n8n-input-label
v-if="property.displayName !== '' && parameter.options && parameter.options.length !== 1"
:label="$locale.nodeText().inputLabelDisplayName(property, path)"
:underline="true"
size="small"
color="text-dark"
/>
<div v-if="multipleValues">
<div
v-for="(_, index) in mutableValues[property.name]"
:key="property.name + index"
class="parameter-item"
>
<div
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
>
<div v-if="!isReadOnly" class="delete-option">
<n8n-icon-button
type="tertiary"
text
size="mini"
icon="trash"
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
@click="deleteOption(property.name, index)"
></n8n-icon-button>
<n8n-icon-button
v-if="sortable && index !== 0"
type="tertiary"
text
size="mini"
icon="angle-up"
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
@click="moveOptionUp(property.name, index)"
></n8n-icon-button>
<n8n-icon-button
v-if="sortable && index !== mutableValues[property.name].length - 1"
type="tertiary"
text
size="mini"
icon="angle-down"
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
@click="moveOptionDown(property.name, index)"
></n8n-icon-button>
</div>
<Suspense>
<ParameterInputList
:parameters="property.values"
:node-values="nodeValues"
:path="getPropertyPath(property.name, index)"
:hide-delete="true"
:is-read-only="isReadOnly"
@value-changed="valueChanged"
/>
</Suspense>
</div>
</div>
</div>
<div v-else class="parameter-item">
<div class="parameter-item-wrapper">
<div v-if="!isReadOnly" class="delete-option">
<n8n-icon-button
type="tertiary"
text
size="mini"
icon="trash"
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
@click="deleteOption(property.name)"
></n8n-icon-button>
</div>
<ParameterInputList
:parameters="property.values"
:node-values="nodeValues"
:path="getPropertyPath(property.name)"
:is-read-only="isReadOnly"
class="parameter-item"
:hide-delete="true"
@value-changed="valueChanged"
/>
</div>
</div>
</div>
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="controls">
<n8n-button
v-if="parameter.options && parameter.options.length === 1"
type="tertiary"
block
data-test-id="fixed-collection-add"
:label="getPlaceholderText"
@click="optionSelected(parameter.options[0].name)"
/>
<div v-else class="add-option">
<n8n-select
v-model="selectedOption"
:placeholder="getPlaceholderText"
size="small"
filterable
@update:model-value="optionSelected"
>
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
:value="item.name"
></n8n-option>
</n8n-select>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.fixed-collection-parameter {
padding-left: var(--spacing-s);

View File

@@ -1,10 +1,3 @@
<template>
<div :class="$style.wrapper" @click="navigateTo">
<font-awesome-icon :class="$style.icon" icon="arrow-left" />
<div :class="$style.text" v-text="$locale.baseText('template.buttons.goBackButton')" />
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { VIEWS } from '@/constants';
@@ -16,6 +9,13 @@ const navigateTo = () => {
};
</script>
<template>
<div :class="$style.wrapper" @click="navigateTo">
<font-awesome-icon :class="$style.icon" icon="arrow-left" />
<div :class="$style.text" v-text="$locale.baseText('template.buttons.goBackButton')" />
</div>
</template>
<style lang="scss" module>
.wrapper {
display: flex;

View File

@@ -1,44 +1,3 @@
<template>
<div
:class="$style.wrapper"
:style="iconStyleData"
@click="() => $emit('click')"
@mouseover="showTooltip = true"
@mouseleave="showTooltip = false"
>
<div :class="$style.tooltip">
<n8n-tooltip placement="top" :visible="showTooltip">
<template #content>
<div v-text="nodeType.displayName"></div>
</template>
<span />
</n8n-tooltip>
</div>
<div v-if="nodeIconData !== null" :class="$style.icon" title="">
<div :class="$style.iconWrapper" :style="iconStyleData">
<div v-if="nodeIconData !== null" :class="$style.icon">
<img
v-if="nodeIconData.type === 'file'"
:src="nodeIconData.fileBuffer || nodeIconData.path"
:style="imageStyleData"
/>
<font-awesome-icon
v-else
:icon="nodeIconData.icon || nodeIconData.path"
:style="fontStyleData"
/>
</div>
<div v-else class="node-icon-placeholder">
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
</div>
</div>
</div>
<div v-else :class="$style.placeholder">
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
</div>
</div>
</template>
<script lang="ts">
import { type StyleValue, defineComponent, type PropType } from 'vue';
@@ -151,6 +110,47 @@ export default defineComponent({
});
</script>
<template>
<div
:class="$style.wrapper"
:style="iconStyleData"
@click="() => $emit('click')"
@mouseover="showTooltip = true"
@mouseleave="showTooltip = false"
>
<div :class="$style.tooltip">
<n8n-tooltip placement="top" :visible="showTooltip">
<template #content>
<div v-text="nodeType.displayName"></div>
</template>
<span />
</n8n-tooltip>
</div>
<div v-if="nodeIconData !== null" :class="$style.icon" title="">
<div :class="$style.iconWrapper" :style="iconStyleData">
<div v-if="nodeIconData !== null" :class="$style.icon">
<img
v-if="nodeIconData.type === 'file'"
:src="nodeIconData.fileBuffer || nodeIconData.path"
:style="imageStyleData"
/>
<font-awesome-icon
v-else
:icon="nodeIconData.icon || nodeIconData.path"
:style="fontStyleData"
/>
</div>
<div v-else class="node-icon-placeholder">
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
</div>
</div>
</div>
<div v-else :class="$style.placeholder">
{{ nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
</div>
</div>
</template>
<style lang="scss" module>
.wrapper {
cursor: pointer;

View File

@@ -1,10 +1,3 @@
<template>
<div :class="$style.editor">
<div ref="htmlEditor" data-test-id="html-editor-container"></div>
<slot name="suffix" />
</div>
</template>
<script setup lang="ts">
import { history } from '@codemirror/commands';
import {
@@ -247,6 +240,13 @@ onBeforeUnmount(() => {
});
</script>
<template>
<div :class="$style.editor">
<div ref="htmlEditor" data-test-id="html-editor-container"></div>
<slot name="suffix" />
</div>
</template>
<style lang="scss" module>
.editor {
height: 100%;

View File

@@ -1,44 +1,3 @@
<template>
<Modal
width="700px"
:title="i18n.baseText('importCurlModal.title')"
:event-bus="modalBus"
:name="IMPORT_CURL_MODAL_KEY"
:center="true"
>
<template #content>
<div :class="$style.container">
<N8nInputLabel :label="i18n.baseText('importCurlModal.input.label')" color="text-dark">
<N8nInput
ref="inputRef"
:model-value="curlCommand"
type="textarea"
:rows="5"
:placeholder="i18n.baseText('importCurlModal.input.placeholder')"
@update:model-value="onInput"
@focus="$event.target.select()"
/>
</N8nInputLabel>
</div>
</template>
<template #footer>
<div :class="$style.modalFooter">
<N8nNotice
:class="$style.notice"
:content="i18n.baseText('ImportCurlModal.notice.content')"
/>
<div>
<N8nButton
float="right"
:label="i18n.baseText('importCurlModal.button.label')"
@click="onImport"
/>
</div>
</div>
</template>
</Modal>
</template>
<script lang="ts" setup>
import Modal from '@/components/Modal.vue';
import { IMPORT_CURL_MODAL_KEY } from '@/constants';
@@ -116,6 +75,47 @@ async function onImport() {
}
</script>
<template>
<Modal
width="700px"
:title="i18n.baseText('importCurlModal.title')"
:event-bus="modalBus"
:name="IMPORT_CURL_MODAL_KEY"
:center="true"
>
<template #content>
<div :class="$style.container">
<N8nInputLabel :label="i18n.baseText('importCurlModal.input.label')" color="text-dark">
<N8nInput
ref="inputRef"
:model-value="curlCommand"
type="textarea"
:rows="5"
:placeholder="i18n.baseText('importCurlModal.input.placeholder')"
@update:model-value="onInput"
@focus="$event.target.select()"
/>
</N8nInputLabel>
</div>
</template>
<template #footer>
<div :class="$style.modalFooter">
<N8nNotice
:class="$style.notice"
:content="i18n.baseText('ImportCurlModal.notice.content')"
/>
<div>
<N8nButton
float="right"
:label="i18n.baseText('importCurlModal.button.label')"
@click="onImport"
/>
</div>
</div>
</template>
</Modal>
</template>
<style module lang="scss">
.modalFooter {
justify-content: space-between;

View File

@@ -1,35 +1,3 @@
<template>
<div :class="$style.container">
<span v-if="readonly" :class="$style.headline">
{{ modelValue }}
</span>
<div
v-else
:class="[$style.headline, $style['headline-editable']]"
@keydown.stop
@click="enableNameEdit"
>
<div v-if="!isNameEdit">
<span>{{ modelValue }}</span>
<i><font-awesome-icon icon="pen" /></i>
</div>
<div v-else :class="$style.nameInput">
<n8n-input
ref="nameInput"
:model-value="modelValue"
size="xlarge"
:maxlength="64"
@update:model-value="onNameEdit"
@change="disableNameEdit"
/>
</div>
</div>
<div v-if="!isNameEdit && subtitle" :class="$style.subtitle">
{{ subtitle }}
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { useToast } from '@/composables/useToast';
@@ -79,6 +47,38 @@ const disableNameEdit = () => {
onClickOutside(nameInput, disableNameEdit);
</script>
<template>
<div :class="$style.container">
<span v-if="readonly" :class="$style.headline">
{{ modelValue }}
</span>
<div
v-else
:class="[$style.headline, $style['headline-editable']]"
@keydown.stop
@click="enableNameEdit"
>
<div v-if="!isNameEdit">
<span>{{ modelValue }}</span>
<i><font-awesome-icon icon="pen" /></i>
</div>
<div v-else :class="$style.nameInput">
<n8n-input
ref="nameInput"
:model-value="modelValue"
size="xlarge"
:maxlength="64"
@update:model-value="onNameEdit"
@change="disableNameEdit"
/>
</div>
</div>
<div v-if="!isNameEdit && subtitle" :class="$style.subtitle">
{{ subtitle }}
</div>
</div>
</template>
<style module lang="scss">
.container {
display: flex;

View File

@@ -1,25 +1,3 @@
<template>
<span class="inline-edit" @keydown.stop>
<span v-if="isEditEnabled && !isDisabled">
<ExpandableInputEdit
v-model="newValue"
:placeholder="placeholder"
:maxlength="maxLength"
:autofocus="true"
:event-bus="inputBus"
@update:model-value="onInput"
@esc="onEscape"
@blur="onBlur"
@enter="submit"
/>
</span>
<span v-else class="preview" @click="onClick">
<ExpandableInputPreview :model-value="previewValue || modelValue" />
</span>
</span>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue';
@@ -100,6 +78,28 @@ function onEscape() {
}
</script>
<template>
<span class="inline-edit" @keydown.stop>
<span v-if="isEditEnabled && !isDisabled">
<ExpandableInputEdit
v-model="newValue"
:placeholder="placeholder"
:maxlength="maxLength"
:autofocus="true"
:event-bus="inputBus"
@update:model-value="onInput"
@esc="onEscape"
@blur="onBlur"
@enter="submit"
/>
</span>
<span v-else class="preview" @click="onClick">
<ExpandableInputPreview :model-value="previewValue || modelValue" />
</span>
</span>
</template>
<style lang="scss" scoped>
.preview {
cursor: pointer;

View File

@@ -1,138 +1,3 @@
<template>
<RunData
:node="currentNode"
:nodes="isMappingMode ? rootNodesParents : parentNodes"
:workflow="workflow"
:run-index="runIndex"
:linked-runs="linkedRuns"
:can-link-runs="!mappedNode && canLinkRuns"
:too-much-data-title="$locale.baseText('ndv.input.tooMuchData.title')"
:no-data-in-branch-message="$locale.baseText('ndv.input.noOutputDataInBranch')"
:is-executing="isExecutingPrevious"
:executing-message="$locale.baseText('ndv.input.executingPrevious')"
:push-ref="pushRef"
:override-outputs="connectedCurrentNodeOutputs"
:mapping-enabled="isMappingEnabled"
:distance-from-active="currentNodeDepth"
:is-production-execution-preview="isProductionExecutionPreview"
:is-pane-active="isPaneActive"
pane-type="input"
data-test-id="ndv-input-panel"
@activate-pane="activatePane"
@item-hover="$emit('itemHover', $event)"
@link-run="onLinkRun"
@unlink-run="onUnlinkRun"
@run-change="onRunIndexChange"
@table-mounted="$emit('tableMounted', $event)"
@search="$emit('search', $event)"
>
<template #header>
<div :class="$style.titleSection">
<span :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
<n8n-radio-buttons
v-if="isActiveNodeConfig && !readOnly"
:options="inputModes"
:model-value="inputMode"
@update:model-value="onInputModeChange"
/>
</div>
</template>
<template #input-select>
<InputNodeSelect
v-if="parentNodes.length && currentNodeName"
:model-value="currentNodeName"
:workflow="workflow"
:nodes="parentNodes"
@update:model-value="onInputNodeChange"
/>
</template>
<template v-if="isMappingMode" #before-data>
<!--
Hide the run linking buttons for both input and ouput panels when in 'Mapping Mode' because the run indices wouldn't match.
Although this is not the most elegant solution, it's straightforward and simpler than introducing a new props and logic to handle this.
-->
<component :is="'style'">button.linkRun { display: none }</component>
<div :class="$style.mappedNode">
<InputNodeSelect
:model-value="mappedNode"
:workflow="workflow"
:nodes="rootNodesParents"
@update:model-value="onMappedNodeSelected"
/>
</div>
</template>
<template #node-not-run>
<div
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
:class="$style.noOutputData"
>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData.title')
}}</n8n-text>
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
<template #content>
<div
v-html="
$locale.baseText('dataMapping.dragFromPreviousHint', {
interpolate: { name: focusedMappableInput },
})
"
></div>
</template>
<NodeExecuteButton
type="secondary"
hide-icon
:transparent="true"
:node-name="isActiveNodeConfig ? rootNode : currentNodeName ?? ''"
:label="$locale.baseText('ndv.input.noOutputData.executePrevious')"
telemetry-source="inputs"
data-test-id="execute-previous-node"
@execute="onNodeExecute"
/>
</n8n-tooltip>
<n8n-text v-if="!readOnly" tag="div" size="small">
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
</n8n-text>
</div>
<div v-else :class="$style.notConnected">
<div>
<WireMeUp />
</div>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.notConnected.title')
}}</n8n-text>
<n8n-text tag="div">
{{ $locale.baseText('ndv.input.notConnected.message') }}
<a
href="https://docs.n8n.io/workflows/connections/"
target="_blank"
@click="onConnectionHelpClick"
>
{{ $locale.baseText('ndv.input.notConnected.learnMore') }}
</a>
</n8n-text>
</div>
</template>
<template #no-output-data>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData')
}}</n8n-text>
</template>
<template #recovered-artificial-output-data>
<div :class="$style.recoveredOutputData">
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('executionDetails.executionFailed.recoveredNodeTitle')
}}</n8n-text>
<n8n-text>
{{ $locale.baseText('executionDetails.executionFailed.recoveredNodeMessage') }}
</n8n-text>
</div>
</template>
</RunData>
</template>
<script lang="ts">
import type { INodeUi } from '@/Interface';
import {
@@ -468,6 +333,141 @@ export default defineComponent({
});
</script>
<template>
<RunData
:node="currentNode"
:nodes="isMappingMode ? rootNodesParents : parentNodes"
:workflow="workflow"
:run-index="runIndex"
:linked-runs="linkedRuns"
:can-link-runs="!mappedNode && canLinkRuns"
:too-much-data-title="$locale.baseText('ndv.input.tooMuchData.title')"
:no-data-in-branch-message="$locale.baseText('ndv.input.noOutputDataInBranch')"
:is-executing="isExecutingPrevious"
:executing-message="$locale.baseText('ndv.input.executingPrevious')"
:push-ref="pushRef"
:override-outputs="connectedCurrentNodeOutputs"
:mapping-enabled="isMappingEnabled"
:distance-from-active="currentNodeDepth"
:is-production-execution-preview="isProductionExecutionPreview"
:is-pane-active="isPaneActive"
pane-type="input"
data-test-id="ndv-input-panel"
@activate-pane="activatePane"
@item-hover="$emit('itemHover', $event)"
@link-run="onLinkRun"
@unlink-run="onUnlinkRun"
@run-change="onRunIndexChange"
@table-mounted="$emit('tableMounted', $event)"
@search="$emit('search', $event)"
>
<template #header>
<div :class="$style.titleSection">
<span :class="$style.title">{{ $locale.baseText('ndv.input') }}</span>
<n8n-radio-buttons
v-if="isActiveNodeConfig && !readOnly"
:options="inputModes"
:model-value="inputMode"
@update:model-value="onInputModeChange"
/>
</div>
</template>
<template #input-select>
<InputNodeSelect
v-if="parentNodes.length && currentNodeName"
:model-value="currentNodeName"
:workflow="workflow"
:nodes="parentNodes"
@update:model-value="onInputNodeChange"
/>
</template>
<template v-if="isMappingMode" #before-data>
<!--
Hide the run linking buttons for both input and ouput panels when in 'Mapping Mode' because the run indices wouldn't match.
Although this is not the most elegant solution, it's straightforward and simpler than introducing a new props and logic to handle this.
-->
<component :is="'style'">button.linkRun { display: none }</component>
<div :class="$style.mappedNode">
<InputNodeSelect
:model-value="mappedNode"
:workflow="workflow"
:nodes="rootNodesParents"
@update:model-value="onMappedNodeSelected"
/>
</div>
</template>
<template #node-not-run>
<div
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
:class="$style.noOutputData"
>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData.title')
}}</n8n-text>
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
<template #content>
<div
v-html="
$locale.baseText('dataMapping.dragFromPreviousHint', {
interpolate: { name: focusedMappableInput },
})
"
></div>
</template>
<NodeExecuteButton
type="secondary"
hide-icon
:transparent="true"
:node-name="isActiveNodeConfig ? rootNode : currentNodeName ?? ''"
:label="$locale.baseText('ndv.input.noOutputData.executePrevious')"
telemetry-source="inputs"
data-test-id="execute-previous-node"
@execute="onNodeExecute"
/>
</n8n-tooltip>
<n8n-text v-if="!readOnly" tag="div" size="small">
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
</n8n-text>
</div>
<div v-else :class="$style.notConnected">
<div>
<WireMeUp />
</div>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.notConnected.title')
}}</n8n-text>
<n8n-text tag="div">
{{ $locale.baseText('ndv.input.notConnected.message') }}
<a
href="https://docs.n8n.io/workflows/connections/"
target="_blank"
@click="onConnectionHelpClick"
>
{{ $locale.baseText('ndv.input.notConnected.learnMore') }}
</a>
</n8n-text>
</div>
</template>
<template #no-output-data>
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('ndv.input.noOutputData')
}}</n8n-text>
</template>
<template #recovered-artificial-output-data>
<div :class="$style.recoveredOutputData">
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
$locale.baseText('executionDetails.executionFailed.recoveredNodeTitle')
}}</n8n-text>
<n8n-text>
{{ $locale.baseText('executionDetails.executionFailed.recoveredNodeMessage') }}
</n8n-text>
</div>
</template>
</RunData>
</template>
<style lang="scss" module>
.mappedNode {
padding: 0 var(--spacing-s) var(--spacing-s);

View File

@@ -1,9 +1,3 @@
<template>
<span ref="observed">
<slot></slot>
</span>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
@@ -37,3 +31,9 @@ export default defineComponent({
},
});
</script>
<template>
<span ref="observed">
<slot></slot>
</span>
</template>

View File

@@ -1,9 +1,3 @@
<template>
<div ref="root">
<slot></slot>
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
@@ -70,3 +64,9 @@ export default defineComponent({
},
});
</script>
<template>
<div ref="root">
<slot></slot>
</div>
</template>

View File

@@ -1,65 +1,3 @@
<template>
<Modal
:name="INVITE_USER_MODAL_KEY"
:title="
$locale.baseText(
showInviteUrls ? 'settings.users.copyInviteUrls' : 'settings.users.inviteNewUsers',
)
"
:center="true"
width="460px"
:event-bus="modalBus"
@enter="onSubmit"
>
<template #content>
<n8n-notice v-if="!isAdvancedPermissionsEnabled">
<i18n-t keypath="settings.users.advancedPermissions.warning">
<template #link>
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
{{ $locale.baseText('settings.users.advancedPermissions.warning.link') }}
</n8n-link>
</template>
</i18n-t>
</n8n-notice>
<div v-if="showInviteUrls">
<n8n-users-list :users="invitedUsers">
<template #actions="{ user }">
<n8n-tooltip>
<template #content>
{{ $locale.baseText('settings.users.inviteLink.copy') }}
</template>
<n8n-icon-button
icon="link"
type="tertiary"
data-test-id="copy-invite-link-button"
:data-invite-link="user.inviteAcceptUrl"
@click="onCopyInviteLink(user)"
></n8n-icon-button>
</n8n-tooltip>
</template>
</n8n-users-list>
</div>
<n8n-form-inputs
v-else
:inputs="config"
:event-bus="formBus"
:column-view="true"
@update="onInput"
@submit="onSubmit"
/>
</template>
<template v-if="!showInviteUrls" #footer>
<n8n-button
:loading="loading"
:disabled="!enabledButton"
:label="buttonLabel"
float="right"
@click="onSubmitClick"
/>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
@@ -344,3 +282,65 @@ export default defineComponent({
},
});
</script>
<template>
<Modal
:name="INVITE_USER_MODAL_KEY"
:title="
$locale.baseText(
showInviteUrls ? 'settings.users.copyInviteUrls' : 'settings.users.inviteNewUsers',
)
"
:center="true"
width="460px"
:event-bus="modalBus"
@enter="onSubmit"
>
<template #content>
<n8n-notice v-if="!isAdvancedPermissionsEnabled">
<i18n-t keypath="settings.users.advancedPermissions.warning">
<template #link>
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
{{ $locale.baseText('settings.users.advancedPermissions.warning.link') }}
</n8n-link>
</template>
</i18n-t>
</n8n-notice>
<div v-if="showInviteUrls">
<n8n-users-list :users="invitedUsers">
<template #actions="{ user }">
<n8n-tooltip>
<template #content>
{{ $locale.baseText('settings.users.inviteLink.copy') }}
</template>
<n8n-icon-button
icon="link"
type="tertiary"
data-test-id="copy-invite-link-button"
:data-invite-link="user.inviteAcceptUrl"
@click="onCopyInviteLink(user)"
></n8n-icon-button>
</n8n-tooltip>
</template>
</n8n-users-list>
</div>
<n8n-form-inputs
v-else
:inputs="config"
:event-bus="formBus"
:column-view="true"
@update="onInput"
@submit="onSubmit"
/>
</template>
<template v-if="!showInviteUrls" #footer>
<n8n-button
:loading="loading"
:disabled="!enabledButton"
:label="buttonLabel"
float="right"
@click="onSubmitClick"
/>
</template>
</Modal>
</template>

View File

@@ -1,10 +1,3 @@
<template>
<div :class="$style.editor" :style="isReadOnly ? 'opacity: 0.7' : ''">
<div ref="jsEditorRef" class="ph-no-capture js-editor"></div>
<slot name="suffix" />
</div>
</template>
<script setup lang="ts">
import { history, toggleComment } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
@@ -124,6 +117,13 @@ const extensions = computed(() => {
});
</script>
<template>
<div :class="$style.editor" :style="isReadOnly ? 'opacity: 0.7' : ''">
<div ref="jsEditorRef" class="ph-no-capture js-editor"></div>
<slot name="suffix" />
</div>
</template>
<style lang="scss" module>
.editor {
height: 100%;

View File

@@ -1,10 +1,3 @@
<template>
<div :class="$style.editor">
<div ref="jsonEditorRef" class="ph-no-capture json-editor"></div>
<slot name="suffix" />
</div>
</template>
<script setup lang="ts">
import { history } from '@codemirror/commands';
import { json, jsonParseLinter } from '@codemirror/lang-json';
@@ -115,6 +108,13 @@ function destroyEditor() {
}
</script>
<template>
<div :class="$style.editor">
<div ref="jsonEditorRef" class="ph-no-capture json-editor"></div>
<slot name="suffix" />
</div>
</template>
<style lang="scss" module>
.editor {
height: 100%;

Some files were not shown because too many files have changed in this diff Show More