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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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