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', useAttrs: 'attrs',
}, },
], ],
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
// TODO: fix these // TODO: fix these
'@typescript-eslint/no-unsafe-call': 'off', '@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> <template>
<div :class="['n8n-action-box', $style.container]" data-test-id="action-box"> <div :class="['n8n-action-box', $style.container]" data-test-id="action-box">
<div v-if="emoji" :class="$style.emoji"> <div v-if="emoji" :class="$style.emoji">
@@ -41,32 +67,6 @@
</div> </div>
</template> </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> <style lang="scss" module>
.container { .container {
border: 2px dashed var(--color-foreground-base); 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> <script lang="ts" setup>
// This component is visually similar to the ActionToggle component // This component is visually similar to the ActionToggle component
// but it offers more options when it comes to dropdown items styling // but it offers more options when it comes to dropdown items styling
@@ -129,6 +75,60 @@ const close = () => elementDropdown.value?.handleClose();
defineExpose({ open, close }); defineExpose({ open, close });
</script> </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> <style lang="scss" module>
:global(.el-dropdown__list) { :global(.el-dropdown__list) {
.userActionsMenu { .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> <template>
<span :class="$style.container" data-test-id="action-toggle" @click.stop.prevent> <span :class="$style.container" data-test-id="action-toggle" @click.stop.prevent>
<ElDropdown <ElDropdown
@@ -41,41 +76,6 @@
</span> </span>
</template> </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> <style lang="scss" module>
.container > * { .container > * {
line-height: 1; 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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@@ -76,6 +51,31 @@ const alertBoxClassNames = computed(() => {
}); });
</script> </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> <style lang="scss" module>
@import '../../css/common/var.scss'; @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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import Avatar from 'vue-boring-avatars'; import Avatar from 'vue-boring-avatars';
@@ -57,6 +41,22 @@ const sizes: { [size: string]: number } = {
const getSize = (size: string): number => sizes[size]; const getSize = (size: string): number => sizes[size];
</script> </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> <style lang="scss" module>
.container { .container {
position: relative; 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> <script lang="ts" setup>
import type { TextSize } from 'n8n-design-system/types/text'; import type { TextSize } from 'n8n-design-system/types/text';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
@@ -34,6 +26,14 @@ withDefaults(defineProps<BadgeProps>(), {
}); });
</script> </script>
<template>
<span :class="['n8n-badge', $style[theme]]">
<N8nText :size="size" :bold="bold" :compact="true">
<slot></slot>
</N8nText>
</span>
</template>
<style lang="scss" module> <style lang="scss" module>
.badge { .badge {
display: inline-flex; display: inline-flex;

View File

@@ -1,3 +1,13 @@
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
};
withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div <div
@@ -9,16 +19,6 @@
</transition> </transition>
</template> </template>
<script lang="ts" setup>
type BlockUiProps = {
show: boolean;
};
withDefaults(defineProps<BlockUiProps>(), {
show: false,
});
</script>
<style lang="scss" module> <style lang="scss" module>
.uiBlocker { .uiBlocker {
position: absolute; 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"> <script setup lang="ts">
import { useCssModule, computed, useAttrs, watchEffect } from 'vue'; import { useCssModule, computed, useAttrs, watchEffect } from 'vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@@ -75,6 +51,30 @@ const classes = computed(() => {
}); });
</script> </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"> <style lang="scss">
@import './Button'; @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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
@@ -70,6 +53,23 @@ const getIconSize = computed<IconSize>(() => {
}); });
</script> </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> <style lang="scss" module>
.callout { .callout {
display: flex; 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> <template>
<div :class="classes" v-bind="$attrs"> <div :class="classes" v-bind="$attrs">
<div v-if="$slots.prepend" :class="$style.icon"> <div v-if="$slots.prepend" :class="$style.icon">
@@ -20,26 +40,6 @@
</div> </div>
</template> </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> <style lang="scss" module>
.card { .card {
border-radius: var(--border-radius-large); 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> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { ElCheckbox } from 'element-plus'; import { ElCheckbox } from 'element-plus';
@@ -58,6 +36,28 @@ const onLabelClick = () => {
}; };
</script> </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> <style lang="scss" module>
.n8nCheckbox { .n8nCheckbox {
display: flex !important; 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"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
const props = withDefaults( const props = withDefaults(
@@ -50,6 +27,29 @@ const style = computed(() => ({
})); }));
</script> </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> <style module>
.progressRingCircle { .progressRingCircle {
transition: stroke-dashoffset 0.35s linear; 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> <script lang="ts" setup>
import { computed, ref, useCssModule } from 'vue'; import { computed, ref, useCssModule } from 'vue';
import N8nSelect from '../N8nSelect'; import N8nSelect from '../N8nSelect';
@@ -138,6 +76,68 @@ function getThStyle(column: DatatableColumn) {
} }
</script> </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> <style lang="scss" module>
.datatableWrapper { .datatableWrapper {
display: block; 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> <script lang="ts" setup>
import N8nFormInputs from '../N8nFormInputs'; import N8nFormInputs from '../N8nFormInputs';
import N8nHeading from '../N8nHeading'; import N8nHeading from '../N8nHeading';
@@ -80,6 +40,46 @@ const onButtonClick = () => formBus.emit('submit');
const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event); const onSecondaryButtonClick = (event: Event) => emit('secondaryClick', event);
</script> </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> <style lang="scss" module>
.heading { .heading {
display: flex; 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> <script lang="ts" setup>
import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue'; import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue';
@@ -271,6 +178,99 @@ watch(
defineExpose({ inputRef }); defineExpose({ inputRef });
</script> </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> <style lang="scss" module>
.infoText { .infoText {
margin-top: var(--spacing-2xs); 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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
@@ -50,6 +44,12 @@ const classes = computed(() => {
}); });
</script> </script>
<template>
<component :is="tag" :class="['n8n-heading', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<style lang="scss" module> <style lang="scss" module>
.bold { .bold {
font-weight: var(--font-weight-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> <script lang="ts" setup>
import type { FontAwesomeIconProps } from '@fortawesome/vue-fontawesome'; import type { FontAwesomeIconProps } from '@fortawesome/vue-fontawesome';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@@ -24,6 +18,12 @@ withDefaults(defineProps<IconProps>(), {
}); });
</script> </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> <style lang="scss" module>
.xlarge { .xlarge {
width: var(--font-size-xl) !important; width: var(--font-size-xl) !important;

View File

@@ -1,7 +1,3 @@
<template>
<N8nButton square v-bind="{ ...$attrs, ...$props }" />
</template>
<script lang="ts" setup> <script lang="ts" setup>
import type { IconButtonProps } from 'n8n-design-system/types/button'; import type { IconButtonProps } from 'n8n-design-system/types/button';
import N8nButton from '../N8nButton'; import N8nButton from '../N8nButton';
@@ -17,3 +13,7 @@ withDefaults(defineProps<IconButtonProps>(), {
active: false, active: false,
}); });
</script> </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> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import N8nText from '../N8nText'; 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); const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick', item, event);
</script> </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> <style lang="scss" module>
.container { .container {
background-color: var(--color-background-base); 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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import type { Placement } from 'element-plus'; import type { Placement } from 'element-plus';
@@ -92,6 +58,40 @@ const iconData = computed((): { icon: string; color: string } => {
}); });
</script> </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> <style lang="scss" module>
.infoTip { .infoTip {
display: flex; 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> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { ElInput } from 'element-plus'; import { ElInput } from 'element-plus';
@@ -96,6 +64,38 @@ const select = () => inputElement.value?.select();
defineExpose({ focus, blur, select }); defineExpose({ focus, blur, select });
</script> </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> <style lang="scss" module>
.xlarge { .xlarge {
--input-font-size: var(--font-size-m); --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> <template>
<div :class="$style.container" v-bind="$attrs" data-test-id="input-label"> <div :class="$style.container" v-bind="$attrs" data-test-id="input-label">
<label <label
@@ -45,39 +78,6 @@
</div> </div>
</template> </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> <style lang="scss" module>
.container { .container {
display: flex; 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> <script lang="ts" setup>
import type { RouteLocationRaw } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
@@ -35,6 +25,16 @@ withDefaults(defineProps<LinkProps>(), {
}); });
</script> </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> <style lang="scss" module>
@import '../../utils'; @import '../../utils';
@import '../../css/common/var'; @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> <template>
<ElSkeleton <ElSkeleton
:loading="loading" :loading="loading"
@@ -35,40 +69,6 @@
</ElSkeleton> </ElSkeleton>
</template> </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> <style lang="scss" module>
.h1Last { .h1Last {
width: 40%; 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> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import type { Options as MarkdownOptions } from 'markdown-it'; import type { Options as MarkdownOptions } from 'markdown-it';
@@ -213,6 +193,26 @@ const onCheckboxChange = (index: number) => {
}; };
</script> </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> <style lang="scss" module>
.markdown { .markdown {
color: var(--color-text-base); 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> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@@ -125,6 +70,61 @@ const onSelect = (item: IMenuItem): void => {
}; };
</script> </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> <style lang="scss" module>
.container { .container {
height: 100%; 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> <template>
<div :class="['n8n-menu-item', $style.item]"> <div :class="['n8n-menu-item', $style.item]">
<ElSubMenu <ElSubMenu
@@ -88,70 +152,6 @@
</div> </div>
</template> </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"> <style module lang="scss">
// Element menu-item overrides // Element menu-item overrides
:global(.el-menu-item), :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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@@ -107,6 +70,43 @@ const badgeStyleData = computed((): Record<string, string> => {
}); });
</script> </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> <style lang="scss" module>
.nodeIconWrapper { .nodeIconWrapper {
width: var(--node-icon-size, 26px); 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> <script lang="ts" setup>
import { computed, ref, useCssModule } from 'vue'; import { computed, ref, useCssModule } from 'vue';
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
@@ -81,6 +64,23 @@ const onClick = (event: MouseEvent) => {
}; };
</script> </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> <style lang="scss" module>
.notice { .notice {
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);

View File

@@ -1,3 +1,7 @@
<script lang="ts" setup>
defineOptions({ name: 'N8nPulse' });
</script>
<template> <template>
<div :class="['pulse', $style.pulseContainer]"> <div :class="['pulse', $style.pulseContainer]">
<div :class="$style.pulse"> <div :class="$style.pulse">
@@ -8,10 +12,6 @@
</div> </div>
</template> </template>
<script lang="ts" setup>
defineOptions({ name: 'N8nPulse' });
</script>
<style lang="scss" module> <style lang="scss" module>
$--light-pulse-color: hsla( $--light-pulse-color: hsla(
var(--color-primary-h), 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> <template>
<label <label
role="radio" role="radio"
@@ -23,22 +39,6 @@
</label> </label>
</template> </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> <style lang="scss" module>
.container { .container {
display: inline-block; 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> <script lang="ts" setup>
import RadioButton from './RadioButton.vue'; import RadioButton from './RadioButton.vue';
@@ -53,6 +36,23 @@ const onClick = (
}; };
</script> </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> <style lang="scss" module>
.radioGroup { .radioGroup {
display: inline-flex; 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> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@@ -180,6 +167,19 @@ const resizerMove = (event: MouseEvent) => {
}; };
</script> </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> <style lang="scss" module>
.resize { .resize {
position: relative; 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> <script lang="ts" setup>
import { computed, ref, useAttrs } from 'vue'; import { computed, ref, useAttrs } from 'vue';
import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue'; import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
@@ -59,3 +42,20 @@ const onResizeEnd = () => {
emit('resizeend'); emit('resizeend');
}; };
</script> </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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { type RouteLocationRaw } from 'vue-router'; import { type RouteLocationRaw } from 'vue-router';
@@ -39,3 +25,17 @@ const useRouterLink = computed(() => {
const openNewWindow = computed(() => !useRouterLink.value); const openNewWindow = computed(() => !useRouterLink.value);
</script> </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> <script lang="ts" setup>
import type { TextSize } from 'n8n-design-system/types/text'; import type { TextSize } from 'n8n-design-system/types/text';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@@ -28,6 +16,18 @@ withDefaults(defineProps<SpinnerProps>(), {
}); });
</script> </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"> <style lang="scss">
.lds-ring { .lds-ring {
display: inline-block; 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> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
@@ -122,6 +74,54 @@ const onInputScroll = (event: WheelEvent) => {
}; };
</script> </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> <style lang="scss" module>
.sticky { .sticky {
position: absolute; 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> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@@ -128,6 +72,62 @@ const scrollLeft = () => scroll(-50);
const scrollRight = () => scroll(50); const scrollRight = () => scroll(50);
</script> </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> <style lang="scss" module>
.container { .container {
position: relative; 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> <script lang="ts" setup>
interface TagProps { interface TagProps {
text: string; text: string;
@@ -12,6 +6,12 @@ defineOptions({ name: 'N8nTag' });
defineProps<TagProps>(); defineProps<TagProps>();
</script> </script>
<template>
<span :class="['n8n-tag', $style.tag]" v-bind="$attrs">
{{ text }}
</span>
</template>
<style lang="scss" module> <style lang="scss" module>
.tag { .tag {
min-width: max-content; 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> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import N8nTag from '../N8nTag'; import N8nTag from '../N8nTag';
@@ -67,6 +47,26 @@ const onExpand = () => {
}; };
</script> </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> <style lang="scss" module>
.tags { .tags {
display: inline-flex; 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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import type { TextSize, TextColor, TextAlign } from 'n8n-design-system/types/text'; import type { TextSize, TextColor, TextAlign } from 'n8n-design-system/types/text';
@@ -46,6 +40,12 @@ const classes = computed(() => {
}); });
</script> </script>
<template>
<component :is="tag" :class="['n8n-text', ...classes]" v-bind="$attrs">
<slot></slot>
</component>
</template>
<style lang="scss" module> <style lang="scss" module>
.bold { .bold {
font-weight: var(--font-weight-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"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@@ -65,6 +43,28 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.buttons { .buttons {
display: flex; 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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
@@ -85,6 +57,34 @@ const getPath = (key: string): Array<string | number> => {
}; };
</script> </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> <style lang="scss" module>
$--spacing: var(--spacing-s); $--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> <script lang="ts" setup>
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
@@ -59,6 +32,33 @@ const classes = computed(
); );
</script> </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> <style lang="scss" module>
.container { .container {
display: inline-flex; 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> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import N8nUserInfo from '../N8nUserInfo'; import N8nUserInfo from '../N8nUserInfo';
@@ -112,6 +80,38 @@ const getLabel = (user: IUser) =>
!user.fullName ? user.email : `${user.fullName} (${user.email})`; !user.fullName ? user.email : `${user.fullName} (${user.email})`;
</script> </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> <style lang="scss" module>
.itemContainer { .itemContainer {
--select-option-padding: var(--spacing-2xs) var(--spacing-s); --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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import N8nActionToggle from '../N8nActionToggle'; import N8nActionToggle from '../N8nActionToggle';
@@ -115,6 +79,42 @@ const onUserAction = (user: IUser, action: string) =>
}); });
</script> </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> <style lang="scss" module>
.itemContainer { .itemContainer {
display: flex; 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> <script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'; import { onMounted, onUnmounted } from 'vue';
@@ -64,6 +47,23 @@ onUnmounted(() => {
}); });
</script> </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> <style lang="scss" module>
.table { .table {
text-align: center; 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> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
@@ -42,6 +31,17 @@ const props = withDefaults(defineProps<SpacingPreviewProps>(), {
const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]); const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]);
</script> </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"> <style lang="scss">
$box-size: 64px; $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> <template>
<Modal <Modal
max-width="540px" max-width="540px"
@@ -68,49 +111,6 @@
</Modal> </Modal>
</template> </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"> <style module lang="scss">
.container > * { .container > * {
margin-bottom: var(--spacing-s); 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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -134,6 +98,42 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.spaced { .spaced {
margin-top: var(--spacing-2xs); margin-top: var(--spacing-2xs);

View File

@@ -1,3 +1,9 @@
<script lang="ts">
export default {
props: ['text', 'type'],
};
</script>
<template> <template>
<el-tag <el-tag
v-if="type === 'danger'" v-if="type === 'danger'"
@@ -18,12 +24,6 @@
</el-tag> </el-tag>
</template> </template>
<script lang="ts">
export default {
props: ['text', 'type'],
};
</script>
<style lang="scss" module> <style lang="scss" module>
.badge { .badge {
font-size: 11px; 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"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
@@ -72,6 +35,43 @@ const onClick = () => {
}; };
</script> </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"> <style module lang="scss">
.icon { .icon {
position: absolute; 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"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { IBinaryData, IRunData } from 'n8n-workflow'; import type { IBinaryData, IRunData } from 'n8n-workflow';
@@ -89,6 +69,26 @@ function closeWindow() {
} }
</script> </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"> <style lang="scss">
.binary-data-window { .binary-data-window {
position: absolute; 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"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
@@ -75,6 +50,31 @@ onMounted(async () => {
}); });
</script> </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"> <style lang="scss">
.binary-data { .binary-data {
background-color: var(--color-foreground-xlight); 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"> <script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/constants'; import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/constants';
@@ -90,3 +84,9 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', onResize); window.removeEventListener('resize', onResize);
}); });
</script> </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> <template>
<div <div
:class="{ :class="{
@@ -57,39 +91,6 @@
</KeyboardShortcutTooltip> </KeyboardShortcutTooltip>
</div> </div>
</template> </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> <style lang="scss" module>
.zoomMenu { .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"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
@@ -160,3 +130,33 @@ onMounted(() => {
config.value = form; config.value = form;
}); });
</script> </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"> <script setup lang="ts">
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python'; import { python } from '@codemirror/lang-python';
@@ -411,6 +363,54 @@ function onAiLoadEnd() {
} }
</script> </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"> <style scoped lang="scss">
:deep(.el-tabs) { :deep(.el-tabs) {
.code-editor-tabs .cm-editor { .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> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
@@ -210,6 +160,56 @@ function valueChanged(parameterData: IUpdateInformation) {
} }
</script> </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"> <style lang="scss">
.collection-parameter { .collection-parameter {
padding-left: var(--spacing-s); padding-left: var(--spacing-s);

View File

@@ -1,3 +1,10 @@
<script lang="ts" setup>
defineProps<{
loading: boolean;
title?: string;
}>();
</script>
<template> <template>
<n8n-card :class="$style.card" v-bind="$attrs"> <n8n-card :class="$style.card" v-bind="$attrs">
<template v-if="!loading && title" #header> <template v-if="!loading && title" #header>
@@ -10,13 +17,6 @@
</n8n-card> </n8n-card>
</template> </template>
<script lang="ts" setup>
defineProps<{
loading: boolean;
title?: string;
}>();
</script>
<style lang="scss" module> <style lang="scss" module>
.card { .card {
min-width: 235px; 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"> <script lang="ts">
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import type { PublicInstalledPackage } from 'n8n-workflow'; import type { PublicInstalledPackage } from 'n8n-workflow';
@@ -125,6 +62,69 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.cardContainer { .cardContainer {
display: flex; 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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -189,6 +97,98 @@ export default defineComponent({
}); });
</script> </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"> <style module lang="scss">
.descriptionContainer { .descriptionContainer {
display: flex; 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> <script>
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
@@ -194,6 +160,40 @@ export default defineComponent({
}); });
</script> </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"> <style module lang="scss">
.descriptionContainer { .descriptionContainer {
display: flex; 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"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@@ -118,6 +84,40 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.description { .description {
margin-bottom: var(--spacing-s); 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"> <script setup lang="ts">
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
@@ -71,6 +47,30 @@ function copy() {
} }
</script> </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> <style lang="scss" module>
.copyText { .copyText {
span { 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"> <script setup lang="ts">
import { computed, onBeforeMount, watch } from 'vue'; import { computed, onBeforeMount, watch } from 'vue';
@@ -345,6 +187,164 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
}); });
</script> </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> <style lang="scss" module>
.config { .config {
--notice-margin: 0; --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"> <script setup lang="ts">
import Feedback from '@/components/Feedback.vue'; import Feedback from '@/components/Feedback.vue';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
@@ -66,6 +42,30 @@ function onDocumentationUrlClick(): void {
} }
</script> </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> <style lang="scss" module>
.docs { .docs {
background-color: var(--color-background-light); 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"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@@ -1143,6 +1028,121 @@ function resetCredentialData(): void {
} }
</script> </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"> <style module lang="scss">
.credentialModal { .credentialModal {
--dialog-max-width: 1200px; --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> <template>
<div :class="$style.container"> <div :class="$style.container">
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
@@ -37,26 +57,6 @@
</div> </div>
</template> </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> <style lang="scss" module>
.container { .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"> <script setup lang="ts">
import type { import type {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
@@ -60,6 +35,31 @@ function valueChanged(parameterData: IUpdateInformation) {
} }
</script> </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> <style lang="scss" module>
.container { .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"> <script lang="ts">
import type { import type {
ICredentialsResponse, ICredentialsResponse,
@@ -208,6 +159,55 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.container { .container {
width: 100%; 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> <script lang="ts" setup>
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
@@ -20,6 +12,14 @@ const googleAuthButtons = {
}; };
</script> </script>
<template>
<button
:class="$style.googleAuthBtn"
:title="$locale.baseText('credentialEdit.oAuthButton.signInWithGoogle')"
:style="googleAuthButtons"
/>
</template>
<style module lang="scss"> <style module lang="scss">
.googleAuthBtn { .googleAuthBtn {
--google-auth-btn-height: 46px; --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> <template>
<div :class="$style.container"> <div :class="$style.container">
<GoogleAuthButton v-if="isGoogleOAuthType" /> <GoogleAuthButton v-if="isGoogleOAuthType" />
@@ -9,14 +17,6 @@
</div> </div>
</template> </template>
<script lang="ts" setup>
import GoogleAuthButton from './GoogleAuthButton.vue';
defineProps<{
isGoogleOAuthType: boolean;
}>();
</script>
<style module lang="scss"> <style module lang="scss">
.container { .container {
display: inline-block; 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"> <script lang="ts">
import type { ICredentialType } from 'n8n-workflow'; import type { ICredentialType } from 'n8n-workflow';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@@ -156,6 +106,56 @@ export default defineComponent({
}); });
</script> </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"> <style module lang="scss">
.parameter-value-container { .parameter-value-container {
display: flex; 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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
@@ -125,6 +66,65 @@ export default defineComponent({
}); });
</script> </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"> <style module lang="scss">
.title { .title {
font-size: var(--font-size-xl); 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"> <script setup lang="ts">
import type { XYPosition } from '@/Interface'; import type { XYPosition } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -137,6 +120,23 @@ const onDragEnd = () => {
}; };
</script> </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> <style lang="scss" module>
.dragging { .dragging {
visibility: visible; 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"> <script setup lang="ts">
import type { XYPosition } from '@/Interface'; import type { XYPosition } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -84,3 +78,9 @@ function getStickyPosition(): XYPosition | null {
return [left + props.stickyOffset[0], top + props.stickyOffset[1]]; return [left + props.stickyOffset[0], top + props.stickyOffset[1]];
} }
</script> </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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -199,6 +150,55 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.content { .content {
> *:not(:last-child) { > *: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"> <script lang="ts">
import { type PropType, defineComponent } from 'vue'; import { type PropType, defineComponent } from 'vue';
import type { EnterpriseEditionFeatureValue } from '@/Interface'; import type { EnterpriseEditionFeatureValue } from '@/Interface';
@@ -29,3 +22,10 @@ export default defineComponent({
}, },
}); });
</script> </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"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
@@ -26,6 +19,13 @@ const hiddenValue = computed(() => {
}); });
</script> </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> <style lang="scss" scoped>
$--horiz-padding: 15px; $--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"> <script setup lang="ts">
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
@@ -78,3 +62,19 @@ function onEscape() {
emit('esc'); emit('esc');
} }
</script> </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> <template>
<ExpandableInputBase :model-value="modelValue" :static-size="true"> <ExpandableInputBase :model-value="modelValue" :static-size="true">
<input <input
@@ -9,16 +19,6 @@
</ExpandableInputBase> </ExpandableInputBase>
</template> </template>
<script setup lang="ts">
import ExpandableInputBase from './ExpandableInputBase.vue';
type Props = {
modelValue: string;
};
defineProps<Props>();
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
input, input,
input:hover { 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"> <script setup lang="ts">
import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/ExpressionEditorModalInput.vue'; import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/ExpressionEditorModalInput.vue';
import { computed, ref, toRaw, watch } from 'vue'; import { computed, ref, toRaw, watch } from 'vue';
@@ -211,6 +120,97 @@ async function onDrop(expression: string, event: MouseEvent) {
} }
</script> </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"> <style module lang="scss">
.modal { .modal {
--dialog-close-top: var(--spacing-m); --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"> <script setup lang="ts">
import { history } from '@codemirror/commands'; import { history } from '@codemirror/commands';
import { Prec } from '@codemirror/state'; import { Prec } from '@codemirror/state';
@@ -109,6 +105,10 @@ onMounted(() => {
defineExpose({ editor }); defineExpose({ editor });
</script> </script>
<template>
<div ref="root" :class="$style.editor" @keydown.stop></div>
</template>
<style lang="scss" module> <style lang="scss" module>
:global(.cm-content) { :global(.cm-content) {
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);

View File

@@ -1,3 +1,9 @@
<script lang="ts">
export default {
name: 'ExpressionFunctionIcon',
};
</script>
<template> <template>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@@ -7,10 +13,4 @@
</svg> </svg>
</template> </template>
<script lang="ts">
export default {
name: 'ExpressionFunctionIcon',
};
</script>
<style lang="scss"></style> <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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -75,6 +47,34 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.actionBoxContainer { .actionBoxContainer {
text-align: center; 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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
@@ -339,6 +211,134 @@ export default defineComponent({
}); });
</script> </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"> <style scoped lang="scss">
.fixed-collection-parameter { .fixed-collection-parameter {
padding-left: var(--spacing-s); 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"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
@@ -16,6 +9,13 @@ const navigateTo = () => {
}; };
</script> </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> <style lang="scss" module>
.wrapper { .wrapper {
display: flex; 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"> <script lang="ts">
import { type StyleValue, defineComponent, type PropType } from 'vue'; import { type StyleValue, defineComponent, type PropType } from 'vue';
@@ -151,6 +110,47 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.wrapper { .wrapper {
cursor: pointer; 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"> <script setup lang="ts">
import { history } from '@codemirror/commands'; import { history } from '@codemirror/commands';
import { import {
@@ -247,6 +240,13 @@ onBeforeUnmount(() => {
}); });
</script> </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> <style lang="scss" module>
.editor { .editor {
height: 100%; 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> <script lang="ts" setup>
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import { IMPORT_CURL_MODAL_KEY } from '@/constants'; import { IMPORT_CURL_MODAL_KEY } from '@/constants';
@@ -116,6 +75,47 @@ async function onImport() {
} }
</script> </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"> <style module lang="scss">
.modalFooter { .modalFooter {
justify-content: space-between; 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"> <script setup lang="ts">
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
@@ -79,6 +47,38 @@ const disableNameEdit = () => {
onClickOutside(nameInput, disableNameEdit); onClickOutside(nameInput, disableNameEdit);
</script> </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"> <style module lang="scss">
.container { .container {
display: flex; 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"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue'; import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue';
@@ -100,6 +78,28 @@ function onEscape() {
} }
</script> </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> <style lang="scss" scoped>
.preview { .preview {
cursor: pointer; 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"> <script lang="ts">
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import { import {
@@ -468,6 +333,141 @@ export default defineComponent({
}); });
</script> </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> <style lang="scss" module>
.mappedNode { .mappedNode {
padding: 0 var(--spacing-s) var(--spacing-s); 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"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@@ -37,3 +31,9 @@ export default defineComponent({
}, },
}); });
</script> </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"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@@ -70,3 +64,9 @@ export default defineComponent({
}, },
}); });
</script> </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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@@ -344,3 +282,65 @@ export default defineComponent({
}, },
}); });
</script> </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"> <script setup lang="ts">
import { history, toggleComment } from '@codemirror/commands'; import { history, toggleComment } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
@@ -124,6 +117,13 @@ const extensions = computed(() => {
}); });
</script> </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> <style lang="scss" module>
.editor { .editor {
height: 100%; 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"> <script setup lang="ts">
import { history } from '@codemirror/commands'; import { history } from '@codemirror/commands';
import { json, jsonParseLinter } from '@codemirror/lang-json'; import { json, jsonParseLinter } from '@codemirror/lang-json';
@@ -115,6 +108,13 @@ function destroyEditor() {
} }
</script> </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> <style lang="scss" module>
.editor { .editor {
height: 100%; height: 100%;

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