refactor(editor): Port more components over to composition API (no-changelog) (#8794)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-03-14 09:19:28 +01:00 committed by GitHub
parent edce632ee6
commit e2131b9ab6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1115 additions and 1630 deletions

View file

@ -35,48 +35,27 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import N8nButton from '../N8nButton';
import N8nHeading from '../N8nHeading';
import N8nText from '../N8nText';
import N8nCallout from '../N8nCallout';
import { defineComponent } from 'vue';
import N8nCallout, { type CalloutTheme } from '../N8nCallout';
import type { ButtonType } from '@/types/button';
export default defineComponent({
name: 'N8nActionBox',
components: {
N8nButton,
N8nHeading,
N8nText,
N8nCallout,
},
props: {
emoji: {
type: String,
},
heading: {
type: String,
},
buttonText: {
type: String,
},
buttonType: {
type: String,
},
description: {
type: String,
},
calloutText: {
type: String,
},
calloutTheme: {
type: String,
default: 'info',
},
calloutIcon: {
type: String,
},
},
interface ActionBoxProps {
emoji: string;
heading: string;
buttonText: string;
buttonType: ButtonType;
description: string;
calloutText: string;
calloutTheme: CalloutTheme;
calloutIcon: string;
}
defineOptions({ name: 'N8nActionBox' });
withDefaults(defineProps<ActionBoxProps>(), {
calloutTheme: 'info',
});
</script>

View file

@ -9,7 +9,7 @@ exports[`N8NActionBox > should render correctly 1`] = `
<div class="description">
<n8n-text-stub color="text-base" bold="false" size="medium" compact="false" tag="span"></n8n-text-stub>
</div>
<n8n-button-stub label="Do something" type="primary" size="large" loading="false" disabled="false" outline="false" text="false" block="false" active="false" square="false" element="button"></n8n-button-stub>
<n8n-button-stub block="false" element="button" label="Do something" square="false" active="false" disabled="false" loading="false" outline="false" size="large" text="false" type="primary"></n8n-button-stub>
<!--v-if-->
</div>"
`;

View file

@ -50,15 +50,21 @@
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
<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
// (supports icons, separators, custom styling and all options provided
// by Element UI dropdown component).
// It can be used in different parts of editor UI while ActionToggle
// is designed to be used in card components.
import { ref, useCssModule, useAttrs } from 'vue';
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import N8nIcon from '../N8nIcon';
import { N8nKeyboardShortcut } from '../N8nKeyboardShortcut';
import type { KeyboardShortcut } from '../../types';
import type { IconSize } from '@/types/icon';
export interface IActionDropdownItem {
interface IActionDropdownItem {
id: string;
label: string;
icon?: string;
@ -68,92 +74,56 @@ export interface IActionDropdownItem {
customClass?: string;
}
// This component is visually similar to the ActionToggle component
// but it offers more options when it comes to dropdown items styling
// (supports icons, separators, custom styling and all options provided
// by Element UI dropdown component).
// It can be used in different parts of editor UI while ActionToggle
// is designed to be used in card components.
export default defineComponent({
name: 'N8nActionDropdown',
components: {
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
N8nIcon,
N8nKeyboardShortcut,
},
props: {
items: {
type: Array as PropType<IActionDropdownItem[]>,
required: true,
},
placement: {
type: String,
default: 'bottom',
validator: (value: string): boolean =>
['top', 'top-end', 'top-start', 'bottom', 'bottom-end', 'bottom-start'].includes(value),
},
activatorIcon: {
type: String,
default: 'ellipsis-h',
},
activatorSize: {
type: String,
default: 'medium',
},
iconSize: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['small', 'medium', 'large'].includes(value),
},
trigger: {
type: String,
default: 'click',
validator: (value: string): boolean => ['click', 'hover'].includes(value),
},
hideArrow: {
type: Boolean,
default: false,
},
},
data() {
const testIdPrefix = this.$attrs['data-test-id'];
return { testIdPrefix };
},
methods: {
getItemClasses(item: IActionDropdownItem): Record<string, boolean> {
return {
[this.$style.itemContainer]: true,
[this.$style.disabled]: item.disabled,
[this.$style.hasCustomStyling]: item.customClass !== undefined,
...(item.customClass !== undefined ? { [item.customClass]: true } : {}),
};
},
onSelect(action: string): void {
this.$emit('select', action);
},
onVisibleChange(open: boolean): void {
this.$emit('visibleChange', open);
},
onButtonBlur(event: FocusEvent): void {
const elementDropdown = this.$refs.elementDropdown as InstanceType<typeof ElDropdown>;
const TRIGGER = ['click', 'hover'] as const;
// Hide dropdown when clicking outside of current document
if (elementDropdown?.handleClose && event.relatedTarget === null) {
elementDropdown.handleClose();
}
},
open() {
const elementDropdown = this.$refs.elementDropdown as InstanceType<typeof ElDropdown>;
elementDropdown.handleOpen();
},
close() {
const elementDropdown = this.$refs.elementDropdown as InstanceType<typeof ElDropdown>;
elementDropdown.handleClose();
},
},
interface ActionDropdownProps {
items: IActionDropdownItem[];
placement?: Placement;
activatorIcon?: string;
activatorSize?: IconSize;
iconSize?: IconSize;
trigger?: (typeof TRIGGER)[number];
hideArrow?: boolean;
}
withDefaults(defineProps<ActionDropdownProps>(), {
placement: 'bottom',
activatorIcon: 'ellipsis-h',
activatorSize: 'medium',
iconSize: 'medium',
trigger: 'click',
hideArrow: false,
});
const $attrs = useAttrs();
const testIdPrefix = $attrs['data-test-id'];
const $style = useCssModule();
const getItemClasses = (item: IActionDropdownItem): Record<string, boolean> => {
return {
[$style.itemContainer]: true,
[$style.disabled]: !!item.disabled,
[$style.hasCustomStyling]: item.customClass !== undefined,
...(item.customClass !== undefined ? { [item.customClass]: true } : {}),
};
};
const $emit = defineEmits(['select', 'visibleChange']);
const elementDropdown = ref<InstanceType<typeof ElDropdown>>();
const onSelect = (action: string) => $emit('select', action);
const onVisibleChange = (open: boolean) => $emit('visibleChange', open);
const onButtonBlur = (event: FocusEvent) => {
// Hide dropdown when clicking outside of current document
if (elementDropdown.value?.handleClose && event.relatedTarget === null) {
elementDropdown.value.handleClose();
}
};
const open = () => elementDropdown.value?.handleOpen();
const close = () => elementDropdown.value?.handleClose();
defineExpose({ open, close });
</script>
<style lang="scss" module>

View file

@ -41,60 +41,36 @@
</span>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
import N8nIcon from '../N8nIcon';
<script lang="ts" setup>
import { ElDropdown, ElDropdownMenu, ElDropdownItem, type Placement } from 'element-plus';
import type { UserAction } from '@/types';
import N8nIcon from '../N8nIcon';
import type { IconOrientation, IconSize } from '@/types/icon';
export default defineComponent({
name: 'N8nActionToggle',
components: {
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
N8nIcon,
},
props: {
actions: {
type: Array as PropType<UserAction[]>,
default: () => [],
},
placement: {
type: String,
default: 'bottom',
validator: (value: string): boolean =>
['top', 'top-end', 'top-start', 'bottom', 'bottom-end', 'bottom-start'].includes(value),
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['mini', 'small', 'medium'].includes(value),
},
iconSize: {
type: String,
},
theme: {
type: String,
default: 'default',
validator: (value: string): boolean => ['default', 'dark'].includes(value),
},
iconOrientation: {
type: String,
default: 'vertical',
validator: (value: string): boolean => ['horizontal', 'vertical'].includes(value),
},
},
methods: {
onCommand(value: string) {
this.$emit('action', value);
},
onVisibleChange(value: boolean) {
this.$emit('visible-change', value);
},
},
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', 'visible-change']);
const onCommand = (value: string) => $emit('action', value);
const onVisibleChange = (value: boolean) => $emit('visible-change', value);
</script>
<style lang="scss" module>

View file

@ -57,20 +57,20 @@ const icon = computed(() => {
}
});
const style = useCssModule();
const $style = useCssModule();
const alertBoxClassNames = computed(() => {
const classNames = ['n8n-alert', style.alert];
const classNames = ['n8n-alert', $style.alert];
if (props.type) {
classNames.push(style[props.type]);
classNames.push($style[props.type]);
}
if (props.effect) {
classNames.push(style[props.effect]);
classNames.push($style[props.effect]);
}
if (props.center) {
classNames.push(style.center);
classNames.push($style.center);
}
if (props.background) {
classNames.push(style.background);
classNames.push($style.background);
}
return classNames;
});

View file

@ -12,63 +12,48 @@
</span>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed } from 'vue';
import Avatar from 'vue-boring-avatars';
interface AvatarProps {
firstName: string;
lastName: string;
size: string;
colors: string[];
}
defineOptions({ name: 'N8nAvatar' });
const props = withDefaults(defineProps<AvatarProps>(), {
firstName: '',
lastName: '',
size: 'medium',
colors: () => [
'--color-primary',
'--color-secondary',
'--color-avatar-accent-1',
'--color-avatar-accent-2',
'--color-primary-tint-1',
],
});
const initials = computed(
() =>
(props.firstName ? props.firstName.charAt(0) : '') +
(props.lastName ? props.lastName.charAt(0) : ''),
);
const getColors = (colors: string[]): string[] => {
const style = getComputedStyle(document.body);
return colors.map((color: string) => style.getPropertyValue(color));
};
const sizes: { [size: string]: number } = {
small: 28,
large: 48,
medium: 40,
};
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nAvatar',
components: {
Avatar,
},
props: {
firstName: {
type: String,
default: '',
},
lastName: {
type: String,
default: '',
},
size: {
type: String,
default: 'medium',
},
colors: {
default: () => [
'--color-primary',
'--color-secondary',
'--color-avatar-accent-1',
'--color-avatar-accent-2',
'--color-primary-tint-1',
],
},
},
computed: {
initials() {
return (
(this.firstName ? this.firstName.charAt(0) : '') +
(this.lastName ? this.lastName.charAt(0) : '')
);
},
},
methods: {
getColors(colors: string[]): string[] {
const style = getComputedStyle(document.body);
return colors.map((color: string) => style.getPropertyValue(color));
},
getSize(size: string): number {
return sizes[size];
},
},
});
const getSize = (size: string): number => sizes[size];
</script>
<style lang="scss" module>

View file

@ -6,33 +6,31 @@
</span>
</template>
<script lang="ts">
<script lang="ts" setup>
import type { TextSize } from '@/types/text';
import N8nText from '../N8nText';
import { defineComponent } from 'vue';
const THEME = [
'default',
'success',
'warning',
'danger',
'primary',
'secondary',
'tertiary',
] as const;
export default defineComponent({
components: {
N8nText,
},
props: {
theme: {
type: String,
default: 'default',
validator: (value: string) =>
['default', 'success', 'warning', 'danger', 'primary', 'secondary', 'tertiary'].includes(
value,
),
},
size: {
type: String,
default: 'small',
},
bold: {
type: Boolean,
default: false,
},
},
interface BadgeProps {
theme?: (typeof THEME)[number];
size?: TextSize;
bold: boolean;
}
defineOptions({ name: 'N8nBadge' });
withDefaults(defineProps<BadgeProps>(), {
theme: 'default',
size: 'small',
bold: false,
});
</script>

View file

@ -20,69 +20,27 @@
</template>
<script setup lang="ts">
import { useCssModule, computed, useAttrs, watchEffect } from 'vue';
import N8nIcon from '../N8nIcon';
import N8nSpinner from '../N8nSpinner';
import { useCssModule, computed, useAttrs, watchEffect } from 'vue';
import type { ButtonProps } from '@/types/button';
const $style = useCssModule();
const $attrs = useAttrs();
const props = defineProps({
label: {
type: String,
default: '',
},
type: {
type: String,
default: 'primary',
},
size: {
type: String,
default: 'medium',
},
loading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
text: {
type: Boolean,
default: false,
},
icon: {
type: [String, Array],
},
block: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
float: {
type: String,
},
square: {
type: Boolean,
default: false,
},
element: {
type: String,
default: 'button',
validator: (value: string) => ['button', 'a'].includes(value),
},
href: {
type: String,
required: false,
},
defineOptions({ name: 'N8nButton' });
const props = withDefaults(defineProps<ButtonProps>(), {
label: '',
type: 'primary',
size: 'medium',
loading: false,
disabled: false,
outline: false,
text: false,
block: false,
active: false,
square: false,
element: 'button',
});
watchEffect(() => {

View file

@ -15,72 +15,57 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon';
const CALLOUT_DEFAULT_ICONS: { [key: string]: string } = {
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
export type CalloutTheme = (typeof THEMES)[number];
const CALLOUT_DEFAULT_ICONS = {
info: 'info-circle',
success: 'check-circle',
warning: 'exclamation-triangle',
danger: 'exclamation-triangle',
};
export default defineComponent({
name: 'N8nCallout',
components: {
N8nText,
N8nIcon,
},
props: {
theme: {
type: String,
required: true,
validator: (value: string): boolean =>
['info', 'success', 'secondary', 'warning', 'danger', 'custom'].includes(value),
},
icon: {
type: String,
},
iconSize: {
type: String,
default: 'medium',
},
iconless: {
type: Boolean,
},
slim: {
type: Boolean,
},
roundCorners: {
type: Boolean,
default: true,
},
},
computed: {
classes(): string[] {
return [
'n8n-callout',
this.$style.callout,
this.$style[this.theme],
this.slim ? this.$style.slim : '',
this.roundCorners ? this.$style.round : '',
];
},
getIcon(): string {
return this.icon ?? CALLOUT_DEFAULT_ICONS?.[this.theme] ?? CALLOUT_DEFAULT_ICONS.info;
},
getIconSize(): string {
if (this.iconSize) {
return this.iconSize;
}
if (this.theme === 'secondary') {
return 'medium';
}
return 'large';
},
},
interface CalloutProps {
theme: CalloutTheme;
icon?: string;
iconSize?: string;
iconless?: boolean;
slim?: boolean;
roundCorners?: boolean;
}
defineOptions({ name: 'N8nCallout' });
const props = withDefaults(defineProps<CalloutProps>(), {
iconSize: 'medium',
roundCorners: true,
});
const $style = useCssModule();
const classes = computed(() => [
'n8n-callout',
$style.callout,
$style[props.theme],
props.slim ? $style.slim : '',
props.roundCorners ? $style.round : '',
]);
const getIcon = computed(
() => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info,
);
const getIconSize = computed(() => {
if (props.iconSize) {
return props.iconSize;
}
if (props.theme === 'secondary') {
return 'medium';
}
return 'large';
});
</script>

View file

@ -1,3 +1,3 @@
import N8nCallout from './Callout.vue';
export type { CalloutTheme } from './Callout.vue';
export default N8nCallout;

View file

@ -20,28 +20,24 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
export default defineComponent({
name: 'N8nCard',
inheritAttrs: true,
props: {
hoverable: {
type: Boolean,
default: false,
},
},
computed: {
classes(): Record<string, boolean> {
return {
card: true,
[this.$style.card]: true,
[this.$style.hoverable]: this.hoverable,
};
},
},
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>

View file

@ -20,56 +20,38 @@
</ElCheckbox>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import { ElCheckbox } from 'element-plus';
import N8nInputLabel from '../N8nInputLabel';
export default defineComponent({
name: 'N8nCheckbox',
components: {
ElCheckbox,
N8nInputLabel,
},
props: {
label: {
type: String,
},
disabled: {
type: Boolean,
default: false,
},
tooltipText: {
type: String,
},
indeterminate: {
type: Boolean,
default: false,
},
modelValue: {
type: Boolean,
default: false,
},
labelSize: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},
},
methods: {
onUpdateModelValue(value: boolean) {
this.$emit('update:modelValue', value);
},
onLabelClick() {
const checkboxComponent = this.$refs.checkbox as ElCheckbox;
if (!checkboxComponent) {
return;
}
const LABEL_SIZE = ['small', 'medium'] as const;
(checkboxComponent.$el as HTMLElement).click();
},
},
interface CheckboxProps {
label?: string;
disabled?: boolean;
tooltipText?: string;
indeterminate?: boolean;
modelValue?: boolean;
labelSize?: (typeof LABEL_SIZE)[number];
}
defineOptions({ name: 'N8nCheckbox' });
withDefaults(defineProps<CheckboxProps>(), {
disabled: false,
indeterminate: false,
modelValue: false,
labelSize: 'medium',
});
const $emit = defineEmits(['update:modelValue']);
const onUpdateModelValue = (value: boolean) => $emit('update:modelValue', value);
const checkbox = ref<InstanceType<typeof ElCheckbox>>();
const onLabelClick = () => {
if (!checkbox?.value) return;
(checkbox.value.$el as HTMLElement).click();
};
</script>
<style lang="scss" module>

View file

@ -4,7 +4,7 @@ import { uid } from '../../utils';
import { ElColorPicker } from 'element-plus';
import N8nInput from '../N8nInput';
export type Props = {
export type ColorPickerProps = {
disabled?: boolean;
size?: 'small' | 'medium' | 'mini';
showAlpha?: boolean;
@ -16,21 +16,21 @@ export type Props = {
name?: string;
};
const props = withDefaults(defineProps<Props>(), {
defineOptions({ name: 'N8nColorPicker' });
const props = withDefaults(defineProps<ColorPickerProps>(), {
disabled: false,
size: 'medium',
showAlpha: false,
colorFormat: 'hex',
popperClass: '',
showInput: true,
modelValue: null,
name: uid('color-picker'),
});
const color = ref(props.modelValue);
const colorPickerProps = computed(() => {
const { value, showInput, ...rest } = props;
const { showInput, ...rest } = props;
return rest;
});
@ -62,6 +62,7 @@ const onActiveChange = (value: string) => {
emit('active-change', value);
};
</script>
<template>
<span :class="['n8n-color-picker', $style.component]">
<ElColorPicker
@ -82,6 +83,7 @@ const onActiveChange = (value: string) => {
/>
</span>
</template>
<style lang="scss" module>
.component {
display: inline-flex;

View file

@ -1,118 +1,3 @@
<script lang="ts">
import type { PropType } from 'vue';
import { computed, defineComponent, ref, useCssModule } from 'vue';
import type { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types';
import { getValueByPath } from '../../utils';
import { useI18n } from '../../composables/useI18n';
import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption';
import N8nPagination from '../N8nPagination';
export default defineComponent({
name: 'N8nDatatable',
components: {
N8nSelect,
N8nOption,
N8nPagination,
},
props: {
columns: {
type: Array as PropType<DatatableColumn[]>,
required: true,
},
rows: {
type: Array as PropType<DatatableRow[]>,
required: true,
},
currentPage: {
type: Number,
default: 1,
},
pagination: {
type: Boolean,
default: true,
},
rowsPerPage: {
type: [Number, String] as PropType<number | '*'>,
default: 10,
},
},
emits: ['update:currentPage', 'update:rowsPerPage'],
setup(props, { emit }) {
const { t } = useI18n();
const rowsPerPageOptions = ref([10, 25, 50, 100]);
const style = useCssModule();
const totalPages = computed(() => {
if (props.rowsPerPage === '*') {
return 1;
}
return Math.ceil(props.rows.length / props.rowsPerPage);
});
const totalRows = computed(() => {
return props.rows.length;
});
const visibleRows = computed(() => {
if (props.rowsPerPage === '*') {
return props.rows;
}
const start = (props.currentPage - 1) * props.rowsPerPage;
const end = start + props.rowsPerPage;
return props.rows.slice(start, end);
});
const classes = computed(() => {
return {
datatable: true,
[style.datatableWrapper]: true,
};
});
function onUpdateCurrentPage(value: number) {
emit('update:currentPage', value);
}
function onRowsPerPageChange(value: number | '*') {
emit('update:rowsPerPage', value);
const maxPage = value === '*' ? 1 : Math.ceil(totalRows.value / value);
if (maxPage < props.currentPage) {
onUpdateCurrentPage(maxPage);
}
}
function getTdValue(row: DatatableRow, column: DatatableColumn) {
return getValueByPath<DatatableRowDataType>(row, column.path);
}
function getThStyle(column: DatatableColumn) {
return {
...(column.width ? { width: column.width } : {}),
};
}
return {
t,
classes,
totalPages,
totalRows,
visibleRows,
rowsPerPageOptions,
getTdValue,
getThStyle,
onUpdateCurrentPage,
onRowsPerPageChange,
};
},
});
</script>
<template>
<div :class="classes" v-bind="$attrs">
<table :class="$style.datatable">
@ -175,6 +60,89 @@ export default defineComponent({
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useCssModule } from 'vue';
import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption';
import N8nPagination from '../N8nPagination';
import type { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types';
import { useI18n } from '../../composables/useI18n';
import { getValueByPath } from '../../utils';
interface DatatableProps {
columns: DatatableColumn[];
rows: DatatableRow[];
currentPage?: number;
pagination?: boolean;
rowsPerPage?: number | '*';
}
defineOptions({ name: 'N8nDatatable' });
const props = withDefaults(defineProps<DatatableProps>(), {
currentPage: 1,
pagination: true,
rowsPerPage: 10,
});
const $emit = defineEmits(['update:currentPage', 'update:rowsPerPage']);
const { t } = useI18n();
const rowsPerPageOptions = ref([10, 25, 50, 100]);
const $style = useCssModule();
const totalPages = computed(() => {
if (props.rowsPerPage === '*') {
return 1;
}
return Math.ceil(props.rows.length / props.rowsPerPage);
});
const totalRows = computed(() => {
return props.rows.length;
});
const visibleRows = computed(() => {
if (props.rowsPerPage === '*') {
return props.rows;
}
const start = (props.currentPage - 1) * props.rowsPerPage;
const end = start + props.rowsPerPage;
return props.rows.slice(start, end);
});
const classes = computed(() => ({
datatable: true,
[$style.datatableWrapper]: true,
}));
function onUpdateCurrentPage(value: number) {
$emit('update:currentPage', value);
}
function onRowsPerPageChange(value: number | '*') {
$emit('update:rowsPerPage', value);
const maxPage = value === '*' ? 1 : Math.ceil(totalRows.value / value);
if (maxPage < props.currentPage) {
onUpdateCurrentPage(maxPage);
}
}
function getTdValue(row: DatatableRow, column: DatatableColumn) {
return getValueByPath<DatatableRowDataType>(row, column.path);
}
function getThStyle(column: DatatableColumn) {
return {
...(column.width ? { width: column.width } : {}),
};
}
</script>
<style lang="scss" module>
.datatableWrapper {
display: block;

View file

@ -16,81 +16,81 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
<td class=""><span>1</span></td>
<td class=""><span>Richard Hendricks</span></td>
<td class=""><span>29</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 1</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>2</span></td>
<td class=""><span>Bertram Gilfoyle</span></td>
<td class=""><span>44</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 2</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>3</span></td>
<td class=""><span>Dinesh Chugtai</span></td>
<td class=""><span>31</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 3</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>4</span></td>
<td class=""><span>Jared Dunn </span></td>
<td class=""><span>38</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 4</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>5</span></td>
<td class=""><span>Richard Hendricks</span></td>
<td class=""><span>29</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 5</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>6</span></td>
<td class=""><span>Bertram Gilfoyle</span></td>
<td class=""><span>44</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 6</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>7</span></td>
<td class=""><span>Dinesh Chugtai</span></td>
<td class=""><span>31</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 7</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>8</span></td>
<td class=""><span>Jared Dunn </span></td>
<td class=""><span>38</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 8</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>9</span></td>
<td class=""><span>Richard Hendricks</span></td>
<td class=""><span>29</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 9</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
<tr>
<td class=""><span>10</span></td>
<td class=""><span>Bertram Gilfoyle</span></td>
<td class=""><span>44</span></td>
<td class=""><button class="button button primary medium" aria-live="polite" column="[object Object]">
<!--v-if--><span>Button 10</span>
</button></td>
<td class="">
<n8n-button-stub block="false" element="button" label="" square="false" active="false" disabled="false" loading="false" outline="false" size="medium" text="false" type="primary" column="[object Object]"></n8n-button-stub>
</td>
</tr>
</tbody>
</table>

View file

@ -38,70 +38,40 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import N8nFormInputs from '../N8nFormInputs';
import N8nHeading from '../N8nHeading';
import N8nLink from '../N8nLink';
import N8nButton from '../N8nButton';
import type { IFormInput } from '@/types';
import { createEventBus } from '../../utils';
export default defineComponent({
name: 'N8nFormBox',
components: {
N8nHeading,
N8nFormInputs,
N8nLink,
N8nButton,
},
props: {
title: {
type: String,
default: '',
},
inputs: {
type: Array,
default: () => [],
},
buttonText: {
type: String,
},
buttonLoading: {
type: Boolean,
default: false,
},
secondaryButtonText: {
type: String,
},
redirectText: {
type: String,
default: '',
},
redirectLink: {
type: String,
default: '',
},
},
data() {
return {
formBus: createEventBus(),
};
},
methods: {
onUpdateModelValue(e: { name: string; value: string }) {
this.$emit('update', e);
},
onSubmit(e: { [key: string]: string }) {
this.$emit('submit', e);
},
onButtonClick() {
this.formBus.emit('submit');
},
onSecondaryButtonClick(event: Event) {
this.$emit('secondaryClick', event);
},
},
interface FormBoxProps {
title?: string;
inputs?: IFormInput[];
buttonText?: string;
buttonLoading?: boolean;
secondaryButtonText?: string;
redirectText?: string;
redirectLink?: string;
}
defineOptions({ name: 'N8nFormBox' });
withDefaults(defineProps<FormBoxProps>(), {
title: '',
inputs: () => [],
buttonLoading: false,
redirectText: '',
redirectLink: '',
});
const formBus = createEventBus();
const $emit = defineEmits(['submit', 'update', 'secondaryClick']);
const onUpdateModelValue = (e: { name: string; value: string }) => $emit('update', e);
const onSubmit = (e: { [key: string]: string }) => $emit('submit', e);
const onButtonClick = () => formBus.emit('submit');
const onSecondaryButtonClick = (event: Event) => $emit('secondaryClick', event);
</script>
<style lang="scss" module>

View file

@ -143,7 +143,7 @@ const props = withDefaults(defineProps<Props>(), {
tagSize: 'small',
});
const emit = defineEmits<{
const $emit = defineEmits<{
(event: 'validate', shouldValidate: boolean): void;
(event: 'update:modelValue', value: unknown): void;
(event: 'focus'): void;
@ -203,22 +203,22 @@ function getInputValidationError(): ReturnType<IValidator['validate']> {
function onBlur() {
state.hasBlurred = true;
state.isTyping = false;
emit('blur');
$emit('blur');
}
function onUpdateModelValue(value: FormState) {
state.isTyping = true;
emit('update:modelValue', value);
$emit('update:modelValue', value);
}
function onFocus() {
emit('focus');
$emit('focus');
}
function onEnter(event: Event) {
event.stopPropagation();
event.preventDefault();
emit('enter');
$emit('enter');
}
const validationError = computed<string | null>(() => {
@ -244,14 +244,14 @@ const showErrors = computed(
);
onMounted(() => {
emit('validate', !validationError.value);
$emit('validate', !validationError.value);
if (props.focusInitially && inputRef.value) inputRef.value.focus();
});
watch(
() => validationError.value,
(error) => emit('validate', !error),
(error) => $emit('validate', !error),
);
defineExpose({ inputRef });

View file

@ -4,55 +4,49 @@
</component>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
export default defineComponent({
name: 'N8nHeading',
props: {
tag: {
type: String,
default: 'span',
},
bold: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['2xlarge', 'xlarge', 'large', 'medium', 'small'].includes(value),
},
color: {
type: String,
validator: (value: string): boolean =>
['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight', 'danger'].includes(
value,
),
},
align: {
type: String,
validator: (value: string): boolean => ['right', 'left', 'center'].includes(value),
},
},
computed: {
classes() {
const applied = [];
if (this.align) {
applied.push(`align-${this.align}`);
}
if (this.color) {
applied.push(this.color);
}
const SIZES = ['2xlarge', 'xlarge', 'large', 'medium', 'small'] as const;
const COLORS = [
'primary',
'text-dark',
'text-base',
'text-light',
'text-xlight',
'danger',
] as const;
const ALIGN = ['right', 'left', 'center'] as const;
applied.push(`size-${this.size}`);
interface HeadingProps {
tag?: string;
bold?: boolean;
size?: (typeof SIZES)[number];
color?: (typeof COLORS)[number];
align?: (typeof ALIGN)[number];
}
applied.push(this.bold ? 'bold' : 'regular');
defineOptions({ name: 'N8nHeading' });
const props = withDefaults(defineProps<HeadingProps>(), {
tag: 'span',
bold: false,
size: 'medium',
});
return applied.map((c) => this.$style[c]);
},
},
const $style = useCssModule();
const classes = computed(() => {
const applied: string[] = [];
if (props.align) {
applied.push(`align-${props.align}`);
}
if (props.color) {
applied.push(props.color);
}
applied.push(`size-${props.size}`);
applied.push(props.bold ? 'bold' : 'regular');
return applied.map((c) => $style[c]);
});
</script>

View file

@ -4,34 +4,22 @@
</N8nText>
</template>
<script lang="ts">
<script lang="ts" setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import type { IconSize, IconColor } from '@/types/icon';
import N8nText from '../N8nText';
import { defineComponent } from 'vue';
interface IconProps {
icon: string;
size?: IconSize;
spin?: boolean;
color?: IconColor;
}
export default defineComponent({
name: 'N8nIcon',
components: {
FontAwesomeIcon,
N8nText,
},
props: {
icon: {
required: true,
},
size: {
type: String,
default: 'medium',
},
spin: {
type: Boolean,
default: false,
},
color: {
type: String,
},
},
defineOptions({ name: 'N8nIcon' });
withDefaults(defineProps<IconProps>(), {
size: 'medium',
spin: false,
});
</script>

View file

@ -2,53 +2,18 @@
<N8nButton square v-bind="{ ...$attrs, ...$props }" />
</template>
<script lang="ts">
<script lang="ts" setup>
import type { IconButtonProps } from '@/types/button';
import N8nButton from '../N8nButton';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nIconButton',
components: {
N8nButton,
},
props: {
type: {
type: String,
default: 'primary',
},
size: {
type: String,
default: 'medium',
},
loading: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
text: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
icon: {
type: [String, Array],
required: true,
},
float: {
type: String,
validator: (value: string): boolean => ['left', 'right'].includes(value),
},
},
defineOptions({ name: 'N8nIconButton' });
withDefaults(defineProps<IconButtonProps>(), {
type: 'primary',
size: 'medium',
loading: false,
outline: false,
text: false,
disabled: false,
active: false,
});
</script>

View file

@ -38,75 +38,53 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { onMounted } from 'vue';
import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import type { EventBus } from '../../utils';
import { createEventBus } from '../../utils';
import type { IconColor } from '@/types/icon';
import { createEventBus, type EventBus } from '../../utils';
export interface IAccordionItem {
interface IAccordionItem {
id: string;
label: string;
icon: string;
iconColor?: string;
iconColor?: IconColor;
tooltip?: string;
}
export default defineComponent({
name: 'N8nInfoAccordion',
components: {
N8nText,
N8nIcon,
},
props: {
title: {
type: String,
},
description: {
type: String,
},
items: {
type: Array as PropType<IAccordionItem[]>,
default: () => [],
},
initiallyExpanded: {
type: Boolean,
default: false,
},
headerIcon: {
type: Object as PropType<{ icon: string; color: string }>,
required: false,
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
data() {
return {
expanded: false,
};
},
mounted() {
this.eventBus.on('expand', () => {
this.expanded = true;
});
this.expanded = this.initiallyExpanded;
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
onClick(e: MouseEvent) {
this.$emit('click:body', e);
},
onTooltipClick(item: string, event: MouseEvent) {
this.$emit('tooltipClick', item, event);
},
},
interface InfoAccordionProps {
title?: string;
description?: string;
items?: IAccordionItem[];
initiallyExpanded?: boolean;
headerIcon?: { icon: string; color: IconColor };
eventBus?: EventBus;
}
defineOptions({ name: 'N8nInfoAccordion' });
const props = withDefaults(defineProps<InfoAccordionProps>(), {
items: () => [],
initiallyExpanded: false,
eventBus: () => createEventBus(),
});
const $emit = defineEmits(['click:body', 'tooltipClick']);
let expanded = false;
onMounted(() => {
props.eventBus.on('expand', () => {
expanded = true;
});
expanded = props.initiallyExpanded;
});
const toggle = () => {
expanded = !expanded;
};
const onClick = (e: MouseEvent) => $emit('click:body', e);
const onTooltipClick = (item: string, event: MouseEvent) => $emit('tooltipClick', item, event);
</script>
<style lang="scss" module>

View file

@ -32,75 +32,63 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed } from 'vue';
import type { Placement } from 'element-plus';
import N8nIcon from '../N8nIcon';
import N8nTooltip from '../N8nTooltip';
import { defineComponent } from 'vue';
const THEME = ['info', 'info-light', 'warning', 'danger', 'success'] as const;
const TYPE = ['note', 'tooltip'] as const;
export default defineComponent({
name: 'N8nInfoTip',
components: {
N8nIcon,
N8nTooltip,
},
props: {
theme: {
type: String,
default: 'info',
validator: (value: string): boolean =>
['info', 'info-light', 'warning', 'danger', 'success'].includes(value),
},
type: {
type: String,
default: 'note',
validator: (value: string): boolean => ['note', 'tooltip'].includes(value),
},
bold: {
type: Boolean,
default: true,
},
tooltipPlacement: {
type: String,
default: 'top',
},
},
computed: {
iconData(): { icon: string; color: string } {
switch (this.theme) {
case 'info':
return {
icon: 'info-circle',
color: '--color-text-light)',
};
case 'info-light':
return {
icon: 'info-circle',
color: 'var(--color-foreground-dark)',
};
case 'warning':
return {
icon: 'exclamation-triangle',
color: 'var(--color-warning)',
};
case 'danger':
return {
icon: 'exclamation-triangle',
color: 'var(--color-danger)',
};
case 'success':
return {
icon: 'check-circle',
color: 'var(--color-success)',
};
default:
return {
icon: 'info-circle',
color: '--color-text-light)',
};
}
},
},
interface InfoTipProps {
theme?: (typeof THEME)[number];
type?: (typeof TYPE)[number];
bold?: boolean;
tooltipPlacement?: Placement;
}
defineOptions({ name: 'N8nInfoTip' });
const props = withDefaults(defineProps<InfoTipProps>(), {
theme: 'info',
type: 'note',
bold: true,
tooltipPlacement: 'top',
});
const iconData = computed((): { icon: string; color: string } => {
switch (props.theme) {
case 'info':
return {
icon: 'info-circle',
color: '--color-text-light)',
};
case 'info-light':
return {
icon: 'info-circle',
color: 'var(--color-foreground-dark)',
};
case 'warning':
return {
icon: 'exclamation-triangle',
color: 'var(--color-warning)',
};
case 'danger':
return {
icon: 'exclamation-triangle',
color: 'var(--color-danger)',
};
case 'success':
return {
icon: 'check-circle',
color: 'var(--color-success)',
};
default:
return {
icon: 'info-circle',
color: '--color-text-light)',
};
}
});
</script>

View file

@ -22,133 +22,68 @@
</ElInput>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { ElInput } from 'element-plus';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { uid } from '../../utils';
type InputRef = InstanceType<typeof ElInput>;
const INPUT = ['text', 'textarea', 'number', 'password', 'email'] as const;
const SIZE = ['mini', 'small', 'medium', 'large', 'xlarge'] as const;
export default defineComponent({
name: 'N8nInput',
components: {
ElInput,
},
props: {
modelValue: {
type: [String, Number] as PropType<string | number>,
default: '',
},
type: {
type: String,
validator: (value: string): boolean =>
['text', 'textarea', 'number', 'password', 'email'].includes(value),
},
size: {
type: String,
default: 'large',
validator: (value: string): boolean =>
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
placeholder: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 2,
},
maxlength: {
type: Number,
default: Infinity,
},
title: {
type: String,
default: '',
},
name: {
type: String,
default: () => uid('input'),
},
autocomplete: {
type: String,
default: 'off',
},
},
computed: {
computedSize(): string | undefined {
if (this.size === 'xlarge') {
return undefined;
}
interface InputProps {
modelValue?: string | number;
type?: (typeof INPUT)[number];
size?: (typeof SIZE)[number];
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
clearable?: boolean;
rows?: number;
maxlength?: number;
title?: string;
name?: string;
autocomplete?: 'off' | 'autocomplete';
}
return this.size;
},
classes(): string[] {
const classes = [];
if (this.size === 'xlarge') {
classes.push('xlarge');
}
if (this.type === 'password') {
classes.push('ph-no-capture');
}
return classes;
},
},
methods: {
focus() {
const innerInput = this.$refs.innerInput as InputRef | undefined;
if (!innerInput) return;
const inputElement = innerInput.$el.querySelector(
this.type === 'textarea' ? 'textarea' : 'input',
);
if (!inputElement) return;
inputElement.focus();
},
blur() {
const innerInput = this.$refs.innerInput as InputRef | undefined;
if (!innerInput) return;
const inputElement = innerInput.$el.querySelector(
this.type === 'textarea' ? 'textarea' : 'input',
);
if (!inputElement) return;
inputElement.blur();
},
select() {
const innerInput = this.$refs.innerInput as InputRef | undefined;
if (!innerInput) return;
const inputElement = innerInput.$el.querySelector(
this.type === 'textarea' ? 'textarea' : 'input',
);
if (!inputElement) return;
inputElement.select();
},
},
defineOptions({ name: 'N8nInput' });
const props = withDefaults(defineProps<InputProps>(), {
modelValue: '',
size: 'large',
placeholder: '',
disabled: false,
readonly: false,
clearable: false,
rows: 2,
maxlength: Infinity,
title: '',
name: () => uid('input'),
autocomplete: 'off',
});
const computedSize = computed(() => (props.size === 'xlarge' ? undefined : props.size));
const classes = computed(() => {
const applied: string[] = [];
if (props.size === 'xlarge') {
applied.push('xlarge');
}
if (props.type === 'password') {
applied.push('ph-no-capture');
}
return applied;
});
const innerInput = ref<InstanceType<typeof ElInput>>();
const inputElement = computed(() => {
if (!innerInput?.value) return;
const inputType = props.type === 'textarea' ? 'textarea' : 'input';
return (innerInput.value.$el as HTMLElement).querySelector(inputType);
});
const focus = () => inputElement.value?.focus();
const blur = () => inputElement.value?.blur();
const select = () => inputElement.value?.select();
defineExpose({ focus, blur, select });
</script>
<style lang="scss" module>

View file

@ -45,65 +45,37 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import N8nText from '../N8nText';
import N8nTooltip from '../N8nTooltip';
import N8nIcon from '../N8nIcon';
import N8nTooltip from '../N8nTooltip';
import type { TextColor } from '@/types/text';
import { addTargetBlank } from '../utils/helpers';
const SIZE = ['small', 'medium'] as const;
import { defineComponent } from 'vue';
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;
}
export default defineComponent({
name: 'N8nInputLabel',
components: {
N8nText,
N8nIcon,
N8nTooltip,
},
props: {
compact: {
type: Boolean,
default: false,
},
color: {
type: String,
},
label: {
type: String,
},
tooltipText: {
type: String,
},
inputName: {
type: String,
},
required: {
type: Boolean,
},
bold: {
type: Boolean,
default: true,
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},
underline: {
type: Boolean,
},
showTooltip: {
type: Boolean,
},
showOptions: {
type: Boolean,
},
},
methods: {
addTargetBlank,
},
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>

View file

@ -8,43 +8,27 @@
</N8nRoute>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import N8nText from '../N8nText';
import N8nRoute from '../N8nRoute';
import N8nRoute, { type RouteTo } from '../N8nRoute';
import type { TextSize } from '@/types/text';
export default defineComponent({
name: 'N8nLink',
components: {
N8nText,
N8nRoute,
},
props: {
size: {
type: String,
},
to: {
type: String || Object,
},
newWindow: {
type: Boolean || undefined,
default: undefined,
},
bold: {
type: Boolean,
default: false,
},
underline: {
type: Boolean,
default: false,
},
theme: {
type: String,
default: 'primary',
validator: (value: string): boolean =>
['primary', 'danger', 'text', 'secondary'].includes(value),
},
},
const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
interface LinkProps {
size?: TextSize;
to?: RouteTo;
newWindow?: boolean;
bold?: boolean;
underline?: boolean;
theme?: (typeof THEME)[number];
}
defineOptions({ name: 'N8nLink' });
withDefaults(defineProps<LinkProps>(), {
bold: false,
underline: false,
theme: 'primary',
});
</script>

View file

@ -35,52 +35,37 @@
</ElSkeleton>
</template>
<script lang="ts">
<script lang="ts" setup>
import { ElSkeleton, ElSkeletonItem } from 'element-plus';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nLoading',
components: {
ElSkeleton,
ElSkeletonItem,
},
props: {
animated: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: true,
},
rows: {
type: Number,
default: 1,
},
shrinkLast: {
type: Boolean,
default: true,
},
variant: {
type: String,
default: 'p',
validator: (value: string): boolean =>
[
'custom',
'p',
'text',
'h1',
'h3',
'text',
'caption',
'button',
'image',
'circle',
'rect',
].includes(value),
},
},
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>

View file

@ -16,172 +16,141 @@
</div>
</template>
<script lang="ts">
import N8nLoading from '../N8nLoading';
import type { PluginSimple } from 'markdown-it';
<script lang="ts" setup>
import { computed } from 'vue';
import type { Options as MarkdownOptions } from 'markdown-it';
import Markdown from 'markdown-it';
import markdownLink from 'markdown-it-link-attributes';
import markdownEmoji from 'markdown-it-emoji';
import markdownTasklists from 'markdown-it-task-lists';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import markdownTaskLists from 'markdown-it-task-lists';
import xss, { friendlyAttrValue } from 'xss';
import N8nLoading from '../N8nLoading';
import { escapeMarkdown } from '../../utils/markdown';
const DEFAULT_OPTIONS_MARKDOWN = {
html: true,
linkify: true,
typographer: true,
breaks: true,
} as const;
const DEFAULT_OPTIONS_LINK_ATTRIBUTES = {
attrs: {
target: '_blank',
rel: 'noopener',
},
} as const;
const DEFAULT_OPTIONS_TASKLISTS = {
label: true,
labelAfter: true,
} as const;
export interface IImage {
interface IImage {
id: string;
url: string;
}
export interface Options {
markdown: typeof DEFAULT_OPTIONS_MARKDOWN;
linkAttributes: typeof DEFAULT_OPTIONS_LINK_ATTRIBUTES;
tasklists: typeof DEFAULT_OPTIONS_TASKLISTS;
interface Options {
markdown: MarkdownOptions;
linkAttributes: markdownLink.Config;
tasklists: markdownTaskLists.Config;
}
export default defineComponent({
name: 'N8nMarkdown',
components: {
N8nLoading,
},
props: {
content: {
type: String,
default: '',
interface MarkdownProps {
content?: string;
withMultiBreaks?: boolean;
images?: IImage[];
loading?: boolean;
loadingBlocks?: number;
loadingRows?: number;
theme?: string;
options?: Options;
}
const props = withDefaults(defineProps<MarkdownProps>(), {
content: '',
withMultiBreaks: false,
images: () => [],
loading: false,
loadingBlocks: 2,
loadingRows: 3,
theme: 'markdown',
options: () => ({
markdown: {
html: true,
linkify: true,
typographer: true,
breaks: true,
},
withMultiBreaks: {
type: Boolean,
default: false,
linkAttributes: {
attrs: {
target: '_blank',
rel: 'noopener',
},
},
images: {
type: Array as PropType<IImage[]>,
default: () => [],
tasklists: {
label: true,
labelAfter: true,
},
loading: {
type: Boolean,
default: false,
},
loadingBlocks: {
type: Number,
default: 2,
},
loadingRows: {
type: Number,
default: 3,
},
theme: {
type: String,
default: 'markdown',
},
options: {
type: Object as PropType<Options>,
default: (): Options => ({
markdown: DEFAULT_OPTIONS_MARKDOWN,
linkAttributes: DEFAULT_OPTIONS_LINK_ATTRIBUTES,
tasklists: DEFAULT_OPTIONS_TASKLISTS,
}),
},
},
data(): { md: Markdown } {
return {
md: new Markdown(this.options.markdown)
.use(markdownLink, this.options.linkAttributes)
.use(markdownEmoji)
.use(markdownTasklists as PluginSimple, this.options.tasklists),
};
},
computed: {
htmlContent(): string {
if (!this.content) {
return '';
}),
});
const { options } = props;
const md = new Markdown(options.markdown)
.use(markdownLink, options.linkAttributes)
.use(markdownEmoji)
.use(markdownTaskLists, options.tasklists);
const htmlContent = computed(() => {
if (!props.content) {
return '';
}
const imageUrls: { [key: string]: string } = {};
if (props.images) {
props.images.forEach((image: IImage) => {
if (!image) {
// Happens if an image got deleted but the workflow
// still has a reference to it
return;
}
imageUrls[image.id] = image.url;
});
}
const imageUrls: { [key: string]: string } = {};
if (this.images) {
this.images.forEach((image: IImage) => {
if (!image) {
// Happens if an image got deleted but the workflow
// still has a reference to it
return;
}
imageUrls[image.id] = image.url;
});
}
const fileIdRegex = new RegExp('fileId:([0-9]+)');
let contentToRender = this.content;
if (this.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
}
const html = this.md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, {
onTagAttr: (tag, name, value) => {
if (tag === 'img' && name === 'src') {
if (value.match(fileIdRegex)) {
const id = value.split('fileId:')[1];
const attributeValue = friendlyAttrValue(imageUrls[id]);
return attributeValue ? `src=${attributeValue}` : '';
}
// Only allow http requests to supported image files from the `static` directory
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
const isStaticImageFile = isImageFile && value.startsWith('/static/');
if (!value.startsWith('https://') && !isStaticImageFile) {
return '';
}
}
// Return nothing, means keep the default handling measure
},
onTag(tag, code) {
if (tag === 'img' && code.includes('alt="workflow-screenshot"')) {
return '';
}
// return nothing, keep tag
},
});
return safeHtml;
},
},
methods: {
onClick(event: MouseEvent) {
let clickedLink = null;
if (event.target instanceof HTMLAnchorElement) {
clickedLink = event.target;
}
if (event.target instanceof HTMLElement && event.target.matches('a *')) {
const parentLink = event.target.closest('a');
if (parentLink) {
clickedLink = parentLink;
const fileIdRegex = new RegExp('fileId:([0-9]+)');
let contentToRender = props.content;
if (props.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
}
const html = md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, {
onTagAttr: (tag, name, value) => {
if (tag === 'img' && name === 'src') {
if (value.match(fileIdRegex)) {
const id = value.split('fileId:')[1];
const attributeValue = friendlyAttrValue(imageUrls[id]);
return attributeValue ? `src=${attributeValue}` : '';
}
// Only allow http requests to supported image files from the `static` directory
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
const isStaticImageFile = isImageFile && value.startsWith('/static/');
if (!value.startsWith('https://') && !isStaticImageFile) {
return '';
}
}
this.$emit('markdown-click', clickedLink, event);
// Return nothing, means keep the default handling measure
},
},
onTag(tag, code) {
if (tag === 'img' && code.includes('alt="workflow-screenshot"')) {
return '';
}
// return nothing, keep tag
},
});
return safeHtml;
});
const $emit = defineEmits(['markdown-click']);
const onClick = (event: MouseEvent) => {
let clickedLink: HTMLAnchorElement | null = null;
if (event.target instanceof HTMLAnchorElement) {
clickedLink = event.target;
}
if (event.target instanceof HTMLElement && event.target.matches('a *')) {
const parentLink = event.target.closest('a');
if (parentLink) {
clickedLink = parentLink;
}
}
$emit('markdown-click', clickedLink, event);
};
</script>
<style lang="scss" module>

View file

@ -120,7 +120,7 @@ export default defineComponent({
},
currentRoute(): RouteObject {
return (
(this as typeof this & { $route: RouteObject }).$route || {
this.$route || {
name: '',
path: '',
}

View file

@ -138,7 +138,7 @@ export default defineComponent({
},
currentRoute(): RouteObject {
return (
(this as typeof this & { $route: RouteObject }).$route || {
this.$route || {
name: '',
path: '',
}

View file

@ -38,6 +38,7 @@
<script lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { defineComponent, type PropType } from 'vue';
import type { Placement } from 'element-plus';
import N8nTooltip from '../N8nTooltip';
export default defineComponent({
@ -77,7 +78,7 @@ export default defineComponent({
type: Boolean,
},
tooltipPosition: {
type: String,
type: String as PropType<Placement>,
default: 'top',
},
badge: { type: Object as PropType<{ src: string; type: string }> },

View file

@ -3,6 +3,7 @@ import { defineComponent } from 'vue';
import { ElPagination } from 'element-plus';
export default defineComponent({
name: 'N8nPagination',
components: {
ElPagination,
},

View file

@ -7,43 +7,41 @@
</a>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed } from 'vue';
export default defineComponent({
name: 'N8nRoute',
props: {
to: {
type: String || Object,
},
newWindow: {
type: Boolean || undefined,
default: undefined,
},
},
computed: {
useRouterLink() {
if (this.newWindow) {
// router-link does not support click events and opening in new window
return false;
}
// TODO: replace `object` with a more detailed type
export type RouteTo = string | object;
if (typeof this.to === 'string') {
return this.to.startsWith('/');
}
interface RouteProps {
to?: RouteTo;
newWindow?: boolean;
}
return this.to !== undefined;
},
openNewWindow() {
if (this.newWindow !== undefined) {
return this.newWindow;
}
defineOptions({ name: 'N8nRoute' });
const props = withDefaults(defineProps<RouteProps>(), {});
if (typeof this.to === 'string') {
return !this.to.startsWith('/');
}
return true;
},
},
const useRouterLink = computed(() => {
if (props.newWindow) {
// router-link does not support click events and opening in new window
return false;
}
if (typeof props.to === 'string') {
return props.to.startsWith('/');
}
return props.to !== undefined;
});
const openNewWindow = computed(() => {
if (props.newWindow !== undefined) {
return props.newWindow;
}
if (typeof props.to === 'string') {
return !props.to.startsWith('/');
}
return true;
});
</script>

View file

@ -1,3 +1,3 @@
import N8nRoute from './Route.vue';
export type { RouteTo } from './Route.vue';
export default N8nRoute;

View file

@ -10,31 +10,20 @@
</span>
</template>
<script lang="ts">
<script lang="ts" setup>
import type { TextSize } from '@/types/text';
import N8nIcon from '../N8nIcon';
import { defineComponent } from 'vue';
const TYPE = ['dots', 'ring'] as const;
export default defineComponent({
name: 'N8nSpinner',
components: {
N8nIcon,
},
props: {
size: {
type: String,
validator(value: string): boolean {
return ['small', 'medium', 'large'].includes(value);
},
},
type: {
type: String,
validator(value: string): boolean {
return ['dots', 'ring'].includes(value);
},
default: 'dots',
},
},
interface SpinnerProps {
size?: Exclude<TextSize, 'xsmall' | 'mini' | 'xlarge'>;
type?: (typeof TYPE)[number];
}
defineOptions({ name: 'N8nSpinner' });
withDefaults(defineProps<SpinnerProps>(), {
type: 'dots',
});
</script>

View file

@ -49,7 +49,7 @@
/>
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<N8nText size="xsmall" aligh="right">
<N8nText size="xsmall" align="right">
<span v-html="t('sticky.markdownHint')"></span>
</N8nText>
</div>

View file

@ -4,17 +4,12 @@
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nTag',
props: {
text: {
type: String,
},
},
});
<script lang="ts" setup>
interface TagProps {
text: string;
}
defineOptions({ name: 'N8nTag' });
defineProps<TagProps>();
</script>
<style lang="scss" module>

View file

@ -13,68 +13,55 @@
size="small"
@click.stop.prevent="onExpand"
>
{{ t('tags.showMore', `${hiddenTagsLength}`) }}
{{ t('tags.showMore', [`${hiddenTagsLength}`]) }}
</N8nLink>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed } from 'vue';
import N8nTag from '../N8nTag';
import N8nLink from '../N8nLink';
import Locale from '../../mixins/locale';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { useI18n } from '../../composables/useI18n';
export interface ITag {
id: string;
name: string;
}
export default defineComponent({
name: 'N8nTags',
components: {
N8nTag,
N8nLink,
},
mixins: [Locale],
props: {
tags: {
type: Array as PropType<ITag[]>,
default: () => [],
},
truncate: {
type: Boolean,
default: false,
},
truncateAt: {
type: Number,
default: 3,
},
},
data() {
return {
showAll: false,
};
},
computed: {
visibleTags(): ITag[] {
if (this.truncate && !this.showAll && this.tags.length > this.truncateAt) {
return this.tags.slice(0, this.truncateAt);
}
interface TagsProp {
tags?: ITag[];
truncate?: boolean;
truncateAt?: number;
}
return this.tags;
},
hiddenTagsLength(): number {
return this.tags.length - this.truncateAt;
},
},
methods: {
onExpand() {
this.showAll = true;
this.$emit('expand', true);
},
},
defineOptions({ name: 'N8nTags' });
const props = withDefaults(defineProps<TagsProp>(), {
tags: () => [],
truncate: false,
truncateAt: 3,
});
const $emit = defineEmits(['expand', 'click:tag']);
const { t } = useI18n();
let showAll = false;
const visibleTags = computed((): ITag[] => {
const { tags, truncate, truncateAt } = props;
if (truncate && !showAll && tags.length > truncateAt) {
return tags.slice(0, truncateAt);
}
return tags;
});
const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt);
const onExpand = () => {
showAll = true;
$emit('expand', true);
};
</script>
<style lang="scss" module>

View file

@ -4,69 +4,45 @@
</component>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nText',
props: {
bold: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['xsmall', 'small', 'mini', 'medium', 'large', 'xlarge'].includes(value),
},
color: {
type: String,
validator: (value: string): boolean =>
[
'primary',
'text-dark',
'text-base',
'text-light',
'text-xlight',
'danger',
'success',
'warning',
].includes(value),
},
align: {
type: String,
validator: (value: string): boolean => ['right', 'left', 'center'].includes(value),
},
compact: {
type: Boolean,
default: false,
},
tag: {
type: String,
default: 'span',
},
},
computed: {
classes() {
const applied = [];
if (this.align) {
applied.push(`align-${this.align}`);
}
if (this.color) {
applied.push(this.color);
}
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import type { TextSize, TextColor, TextAlign } from '@/types/text';
if (this.compact) {
applied.push('compact');
}
interface TextProps {
bold?: boolean;
size?: TextSize;
color?: TextColor;
align?: TextAlign;
compact?: boolean;
tag?: string;
}
applied.push(`size-${this.size}`);
defineOptions({ name: 'N8nText' });
const props = withDefaults(defineProps<TextProps>(), {
bold: false,
size: 'medium',
compact: false,
tag: 'span',
});
applied.push(this.bold ? 'bold' : 'regular');
const $style = useCssModule();
const classes = computed(() => {
const applied: string[] = [];
if (props.align) {
applied.push(`align-${props.align}`);
}
if (props.color) {
applied.push(props.color);
}
return applied.map((c) => this.$style[c]);
},
},
if (props.compact) {
applied.push('compact');
}
applied.push(`size-${props.size}`);
applied.push(props.bold ? 'bold' : 'regular');
return applied.map((c) => $style[c]);
});
</script>

View file

@ -25,61 +25,38 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText';
import N8nAvatar from '../N8nAvatar';
import N8nBadge from '../N8nBadge';
import Locale from '../../mixins/locale';
import { defineComponent } from 'vue';
import { useI18n } from '../../composables/useI18n';
export default defineComponent({
name: 'N8nUsersInfo',
components: {
N8nAvatar,
N8nText,
N8nBadge,
},
mixins: [Locale],
props: {
firstName: {
type: String,
},
lastName: {
type: String,
},
email: {
type: String,
},
isOwner: {
type: Boolean,
},
isPendingUser: {
type: Boolean,
},
isCurrentUser: {
type: Boolean,
},
disabled: {
type: Boolean,
},
settings: {
type: Object,
required: false,
},
isSamlLoginEnabled: {
type: Boolean,
required: false,
},
},
computed: {
classes(): Record<string, boolean> {
return {
[this.$style.container]: true,
[this.$style.disabled]: this.disabled,
};
},
},
interface UsersInfoProps {
firstName?: string;
lastName?: string;
email?: string;
isOwner?: boolean;
isPendingUser?: boolean;
isCurrentUser?: boolean;
disabled?: boolean;
settings?: object;
isSamlLoginEnabled?: boolean;
}
const props = withDefaults(defineProps<UsersInfoProps>(), {
disabled: false,
});
const { t } = useI18n();
const $style = useCssModule();
const classes = computed(
(): Record<string, boolean> => ({
[$style.container]: true,
[$style.disabled]: props.disabled,
}),
);
</script>
<style lang="scss" module>

View file

@ -34,101 +34,78 @@
</div>
</template>
<script lang="ts">
import type { IUser, UserAction } from '../../types';
<script lang="ts" setup>
import { computed } from 'vue';
import N8nActionToggle from '../N8nActionToggle';
import N8nBadge from '../N8nBadge';
import N8nUserInfo from '../N8nUserInfo';
import Locale from '../../mixins/locale';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import type { IUser, UserAction } from '../../types';
import { useI18n } from '../../composables/useI18n';
export default defineComponent({
name: 'N8nUsersList',
components: {
N8nActionToggle,
N8nBadge,
N8nUserInfo,
},
mixins: [Locale],
props: {
readonly: {
type: Boolean,
default: false,
},
users: {
type: Array,
required: true,
default(): IUser[] {
return [];
},
},
currentUserId: {
type: String,
},
actions: {
type: Array as PropType<UserAction[]>,
default: () => [],
},
isSamlLoginEnabled: {
type: Boolean,
default: false,
},
},
computed: {
sortedUsers(): IUser[] {
return [...(this.users as IUser[])].sort((a: IUser, b: IUser) => {
if (!a.email || !b.email) {
throw new Error('Expected all users to have email');
}
interface UsersListProps {
users: IUser[];
readonly?: boolean;
currentUserId?: string;
actions?: UserAction[];
isSamlLoginEnabled?: boolean;
}
// invited users sorted by email
if (a.isPendingUser && b.isPendingUser) {
return a.email > b.email ? 1 : -1;
}
if (a.isPendingUser) {
return -1;
}
if (b.isPendingUser) {
return 1;
}
if (a.isOwner) {
return -1;
}
if (b.isOwner) {
return 1;
}
if (a.lastName && b.lastName && a.firstName && b.firstName) {
if (a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName !== b.firstName) {
return a.firstName > b.firstName ? 1 : -1;
}
}
return a.email > b.email ? 1 : -1;
});
},
},
methods: {
getActions(user: IUser): UserAction[] {
if (user.isOwner) {
return [];
}
const defaultGuard = () => true;
return this.actions.filter((action) => (action.guard || defaultGuard)(user));
},
onUserAction(user: IUser, action: string): void {
this.$emit(action, user.id);
},
},
const props = withDefaults(defineProps<UsersListProps>(), {
readonly: false,
users: () => [],
actions: () => [],
isSamlLoginEnabled: false,
});
const { t } = useI18n();
const sortedUsers = computed(() =>
[...props.users].sort((a: IUser, b: IUser) => {
if (!a.email || !b.email) {
throw new Error('Expected all users to have email');
}
// invited users sorted by email
if (a.isPendingUser && b.isPendingUser) {
return a.email > b.email ? 1 : -1;
}
if (a.isPendingUser) {
return -1;
}
if (b.isPendingUser) {
return 1;
}
if (a.isOwner) {
return -1;
}
if (b.isOwner) {
return 1;
}
if (a.lastName && b.lastName && a.firstName && b.firstName) {
if (a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName !== b.firstName) {
return a.firstName > b.firstName ? 1 : -1;
}
}
return a.email > b.email ? 1 : -1;
}),
);
const defaultGuard = () => true;
const getActions = (user: IUser): UserAction[] => {
if (user.isOwner) return [];
return props.actions.filter((action) => (action.guard || defaultGuard)(user));
};
const $emit = defineEmits(['*']);
const onUserAction = (user: IUser, action: string) => $emit(action, user.id);
</script>
<style lang="scss" module>

View file

@ -1,3 +0,0 @@
export function addTargetBlank(html: string) {
return html && html.includes('href=') ? html.replace(/href=/g, 'target="_blank" href=') : html;
}

View file

@ -3,7 +3,7 @@ import { useDeviceSupport } from '@/composables/useDeviceSupport';
describe('useDeviceSupport()', () => {
beforeEach(() => {
global.window = Object.create(window);
global.navigator = { userAgent: 'test-agent', maxTouchPoints: 0 };
global.navigator = { userAgent: 'test-agent', maxTouchPoints: 0 } as Navigator;
});
describe('isTouchDevice', () => {

View file

@ -2,8 +2,8 @@ import { t } from '../locale';
export default {
methods: {
t(...args: string[]) {
return t.apply(this, args);
t(path: string, ...args: string[]) {
return t.call(this, path, ...args);
},
},
};

View file

@ -1,4 +1,15 @@
declare module 'markdown-it-task-lists' {
import type { PluginSimple } from 'markdown-it';
export default plugin as PluginSimple<{}>;
import type { PluginWithOptions } from 'markdown-it';
declare namespace markdownItTaskLists {
interface Config {
enabled?: boolean;
label?: boolean;
labelAfter?: boolean;
}
}
declare const markdownItTaskLists: PluginWithOptions<markdownItTaskLists.Config>;
export = markdownItTaskLists;
}

View file

@ -1,17 +1,36 @@
import type { TextFloat } from './text';
const BUTTON_ELEMENT = ['button', 'a'] as const;
export type ButtonElement = (typeof BUTTON_ELEMENT)[number];
const BUTTON_TYPE = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'] as const;
export type ButtonType = (typeof BUTTON_TYPE)[number];
const BUTTON_SIZE = ['small', 'medium', 'large'] as const;
export type ButtonSize = (typeof BUTTON_SIZE)[number];
export interface IconButtonProps {
active?: boolean;
disabled?: boolean;
float?: TextFloat;
icon?: string;
loading?: boolean;
outline?: boolean;
size?: ButtonSize;
text?: boolean;
type?: ButtonType;
}
export interface ButtonProps extends IconButtonProps {
block?: boolean;
element?: ButtonElement;
href?: string;
label?: string;
square?: boolean;
}
export type IN8nButton = {
attrs: {
label: string;
type?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger';
size?: 'mini' | 'small' | 'medium' | 'large' | 'xlarge';
loading?: boolean;
disabled?: boolean;
outline?: boolean;
text?: boolean;
icon?: string;
block?: boolean;
active?: boolean;
float?: 'left' | 'right';
square?: boolean;
attrs: ButtonProps & {
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-test-id'?: string;
};

View file

@ -0,0 +1,8 @@
const ICON_SIZE = ['xsmall', 'small', 'medium', 'large'] as const;
export type IconSize = (typeof ICON_SIZE)[number];
const ICON_COLOR = ['primary', 'danger', 'success', 'warning', 'text-base'] as const;
export type IconColor = (typeof ICON_COLOR)[number];
const ICON_ORIENTATION = ['horizontal', 'vertical'] as const;
export type IconOrientation = (typeof ICON_ORIENTATION)[number];

View file

@ -0,0 +1,20 @@
const TEXT_SIZE = ['xsmall', 'small', 'mini', 'medium', 'large', 'xlarge'] as const;
export type TextSize = (typeof TEXT_SIZE)[number];
const TEXT_COLOR = [
'primary',
'text-dark',
'text-base',
'text-light',
'text-xlight',
'danger',
'success',
'warning',
] as const;
export type TextColor = (typeof TEXT_COLOR)[number];
const TEXT_ALIGN = ['right', 'left', 'center'] as const;
export type TextAlign = (typeof TEXT_ALIGN)[number];
const TEXT_FLOAT = ['left', 'right'] as const;
export type TextFloat = (typeof TEXT_FLOAT)[number];

View file

@ -10,13 +10,15 @@
"incremental": false,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"types": ["webpack-env", "vitest/globals"],
"typeRoots": ["@testing-library", "@types"],
"types": ["vitest/globals"],
"typeRoots": ["@testing-library", "@types", "../../node_modules"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
// TODO: remove all options below this line
"strict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"noImplicitReturns": false
},