mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
refactor(editor): Port more components over to composition API (no-changelog) (#8794)
This commit is contained in:
parent
edce632ee6
commit
e2131b9ab6
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>"
|
||||
`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import N8nCallout from './Callout.vue';
|
||||
|
||||
export type { CalloutTheme } from './Callout.vue';
|
||||
export default N8nCallout;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 \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 \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>
|
||||
|
|
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||
},
|
||||
currentRoute(): RouteObject {
|
||||
return (
|
||||
(this as typeof this & { $route: RouteObject }).$route || {
|
||||
this.$route || {
|
||||
name: '',
|
||||
path: '',
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ export default defineComponent({
|
|||
},
|
||||
currentRoute(): RouteObject {
|
||||
return (
|
||||
(this as typeof this & { $route: RouteObject }).$route || {
|
||||
this.$route || {
|
||||
name: '',
|
||||
path: '',
|
||||
}
|
||||
|
|
|
@ -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 }> },
|
||||
|
|
|
@ -3,6 +3,7 @@ import { defineComponent } from 'vue';
|
|||
import { ElPagination } from 'element-plus';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nPagination',
|
||||
components: {
|
||||
ElPagination,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import N8nRoute from './Route.vue';
|
||||
|
||||
export type { RouteTo } from './Route.vue';
|
||||
export default N8nRoute;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export function addTargetBlank(html: string) {
|
||||
return html && html.includes('href=') ? html.replace(/href=/g, 'target="_blank" href=') : html;
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
8
packages/design-system/src/types/icon.ts
Normal file
8
packages/design-system/src/types/icon.ts
Normal 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];
|
20
packages/design-system/src/types/text.ts
Normal file
20
packages/design-system/src/types/text.ts
Normal 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];
|
|
@ -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
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue