refactor(editor): Continue porting components over to composition API (no-changelog) (#8893)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-03-15 12:43:08 +01:00 committed by GitHub
parent 80c4bc443a
commit 6c693e1afd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 989 additions and 1253 deletions

View file

@ -19,6 +19,7 @@
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import type { IconSize } from '@/types/icon';
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const; const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
export type CalloutTheme = (typeof THEMES)[number]; export type CalloutTheme = (typeof THEMES)[number];
@ -33,7 +34,7 @@ const CALLOUT_DEFAULT_ICONS = {
interface CalloutProps { interface CalloutProps {
theme: CalloutTheme; theme: CalloutTheme;
icon?: string; icon?: string;
iconSize?: string; iconSize?: IconSize;
iconless?: boolean; iconless?: boolean;
slim?: boolean; slim?: boolean;
roundCorners?: boolean; roundCorners?: boolean;
@ -58,7 +59,7 @@ const getIcon = computed(
() => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info, () => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info,
); );
const getIconSize = computed(() => { const getIconSize = computed<IconSize>(() => {
if (props.iconSize) { if (props.iconSize) {
return props.iconSize; return props.iconSize;
} }

View file

@ -34,11 +34,11 @@
<slot v-if="hasDefaultSlot" /> <slot v-if="hasDefaultSlot" />
<N8nSelect <N8nSelect
v-else-if="type === 'select' || type === 'multi-select'" v-else-if="type === 'select' || type === 'multi-select'"
ref="inputRef"
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }" :class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
:model-value="modelValue" :model-value="modelValue"
:placeholder="placeholder" :placeholder="placeholder"
:multiple="type === 'multi-select'" :multiple="type === 'multi-select'"
ref="inputRef"
:disabled="disabled" :disabled="disabled"
:name="name" :name="name"
:teleported="teleported" :teleported="teleported"
@ -57,8 +57,8 @@
</N8nSelect> </N8nSelect>
<N8nInput <N8nInput
v-else v-else
:name="name"
ref="inputRef" ref="inputRef"
:name="name"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:model-value="modelValue" :model-value="modelValue"

View file

@ -41,7 +41,7 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import N8nFormInput from '../N8nFormInput'; import N8nFormInput from '../N8nFormInput';
import type { IFormInput } from '../../types'; import type { IFormInput, Validatable } from '../../types';
import ResizeObserver from '../ResizeObserver'; import ResizeObserver from '../ResizeObserver';
import type { EventBus } from '../../utils'; import type { EventBus } from '../../utils';
import { createEventBus } from '../../utils'; import { createEventBus } from '../../utils';
@ -83,7 +83,7 @@ export default defineComponent({
data() { data() {
return { return {
showValidationWarnings: false, showValidationWarnings: false,
values: {} as { [key: string]: unknown }, values: {} as { [key: string]: Validatable },
validity: {} as { [key: string]: boolean }, validity: {} as { [key: string]: boolean },
}; };
}, },
@ -141,12 +141,15 @@ export default defineComponent({
this.showValidationWarnings = true; this.showValidationWarnings = true;
if (this.isReadyToSubmit) { if (this.isReadyToSubmit) {
const toSubmit = this.filteredInputs.reduce<{ [key: string]: unknown }>((accu, input) => { const toSubmit = this.filteredInputs.reduce(
if (this.values[input.name]) { (accu, input) => {
accu[input.name] = this.values[input.name]; if (this.values[input.name]) {
} accu[input.name] = this.values[input.name];
return accu; }
}, {}); return accu;
},
{} as { [key: string]: Validatable },
);
this.$emit('submit', toSubmit); this.$emit('submit', toSubmit);
} }
}, },

View file

@ -39,7 +39,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted, ref } from 'vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import type { IconColor } from '@/types/icon'; import type { IconColor } from '@/types/icon';
@ -70,16 +70,16 @@ const props = withDefaults(defineProps<InfoAccordionProps>(), {
}); });
const $emit = defineEmits(['click:body', 'tooltipClick']); const $emit = defineEmits(['click:body', 'tooltipClick']);
let expanded = false; const expanded = ref(false);
onMounted(() => { onMounted(() => {
props.eventBus.on('expand', () => { props.eventBus.on('expand', () => {
expanded = true; expanded.value = true;
}); });
expanded = props.initiallyExpanded; expanded.value = props.initiallyExpanded;
}); });
const toggle = () => { const toggle = () => {
expanded = !expanded; expanded.value = !expanded.value;
}; };
const onClick = (e: MouseEvent) => $emit('click:body', e); const onClick = (e: MouseEvent) => $emit('click:body', e);

View file

@ -9,15 +9,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { RouteLocationRaw } from 'vue-router';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import N8nRoute, { type RouteTo } from '../N8nRoute'; import N8nRoute from '../N8nRoute';
import type { TextSize } from '@/types/text'; import type { TextSize } from '@/types/text';
const THEME = ['primary', 'danger', 'text', 'secondary'] as const; const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
interface LinkProps { interface LinkProps {
to?: RouteLocationRaw;
size?: TextSize; size?: TextSize;
to?: RouteTo;
newWindow?: boolean; newWindow?: boolean;
bold?: boolean; bold?: boolean;
underline?: boolean; underline?: boolean;

View file

@ -8,7 +8,7 @@
v-html="htmlContent" v-html="htmlContent"
/> />
<div v-else :class="$style.markdown"> <div v-else :class="$style.markdown">
<div v-for="(block, index) in loadingBlocks" :key="index"> <div v-for="(_, index) in loadingBlocks" :key="index">
<N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" /> <N8nLoading :loading="loading" :rows="loadingRows" animated variant="p" />
<div :class="$style.spacer" /> <div :class="$style.spacer" />
</div> </div>

View file

@ -53,104 +53,76 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { ElMenu } from 'element-plus'; import { ElMenu } from 'element-plus';
import N8nMenuItem from '../N8nMenuItem'; import N8nMenuItem from '../N8nMenuItem';
import type { PropType } from 'vue'; import type { IMenuItem } from '../../types';
import { defineComponent } from 'vue';
import type { IMenuItem, RouteObject } from '../../types';
import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil'; import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil';
export default defineComponent({ interface MenuProps {
name: 'N8nMenu', type?: 'primary' | 'secondary';
components: { defaultActive?: string;
ElMenu, collapsed?: boolean;
N8nMenuItem, transparentBackground?: boolean;
}, mode?: 'router' | 'tabs';
props: { tooltipDelay?: number;
type: { items?: IMenuItem[];
type: String, modelValue?: string;
default: 'primary', }
validator: (value: string): boolean => ['primary', 'secondary'].includes(value),
},
defaultActive: {
type: String,
},
collapsed: {
type: Boolean,
default: false,
},
transparentBackground: {
type: Boolean,
default: false,
},
mode: {
type: String,
default: 'router',
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
},
tooltipDelay: {
type: Number,
default: 300,
},
items: {
type: Array as PropType<IMenuItem[]>,
default: (): IMenuItem[] => [],
},
modelValue: {
type: [String, Boolean],
default: '',
},
},
data() {
return {
activeTab: this.value,
};
},
computed: {
upperMenuItems(): IMenuItem[] {
return this.items.filter(
(item: IMenuItem) => item.position === 'top' && item.available !== false,
);
},
lowerMenuItems(): IMenuItem[] {
return this.items.filter(
(item: IMenuItem) => item.position === 'bottom' && item.available !== false,
);
},
currentRoute(): RouteObject {
return (
this.$route || {
name: '',
path: '',
}
);
},
},
mounted() {
if (this.mode === 'router') {
const found = this.items.find((item) =>
doesMenuItemMatchCurrentRoute(item, this.currentRoute),
);
this.activeTab = found ? found.id : ''; const props = withDefaults(defineProps<MenuProps>(), {
} else { type: 'primary',
this.activeTab = this.items.length > 0 ? this.items[0].id : ''; collapsed: false,
} transparentBackground: false,
mode: 'router',
this.$emit('update:modelValue', this.activeTab); tooltipDelay: 300,
}, items: () => [],
methods: {
onSelect(item: IMenuItem): void {
if (this.mode === 'tabs') {
this.activeTab = item.id;
}
this.$emit('select', item.id);
this.$emit('update:modelValue', item.id);
},
},
}); });
const $route = useRoute();
const $emit = defineEmits<{
(event: 'select', itemId: string);
(event: 'update:modelValue', itemId: string);
}>();
const activeTab = ref(props.modelValue);
const upperMenuItems = computed(() =>
props.items.filter((item: IMenuItem) => item.position === 'top' && item.available !== false),
);
const lowerMenuItems = computed(() =>
props.items.filter((item: IMenuItem) => item.position === 'bottom' && item.available !== false),
);
const currentRoute = computed(() => {
return $route ?? { name: '', path: '' };
});
onMounted(() => {
if (props.mode === 'router') {
const found = props.items.find((item) =>
doesMenuItemMatchCurrentRoute(item, currentRoute.value),
);
activeTab.value = found ? found.id : '';
} else {
activeTab.value = props.items.length > 0 ? props.items[0].id : '';
}
$emit('update:modelValue', activeTab.value);
});
const onSelect = (item: IMenuItem): void => {
if (props.mode === 'tabs') {
activeTab.value = item.id;
}
$emit('select', item.id);
$emit('update:modelValue', item.id);
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -52,7 +52,7 @@
}" }"
data-test-id="menu-item" data-test-id="menu-item"
:index="item.id" :index="item.id"
@click="handleSelect(item)" @click="handleSelect?.(item)"
> >
<N8nIcon <N8nIcon
v-if="item.icon" v-if="item.icon"
@ -80,94 +80,67 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import { useRoute } from 'vue-router';
import { ElSubMenu, ElMenuItem } from 'element-plus'; import { ElSubMenu, ElMenuItem } from 'element-plus';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import ConditionalRouterLink from '../ConditionalRouterLink'; import ConditionalRouterLink from '../ConditionalRouterLink';
import type { IMenuItem, RouteObject } from '../../types'; import type { IMenuItem } from '../../types';
import { doesMenuItemMatchCurrentRoute } from './routerUtil'; import { doesMenuItemMatchCurrentRoute } from './routerUtil';
export default defineComponent({ interface MenuItemProps {
name: 'N8nMenuItem', item: IMenuItem;
components: { compact?: boolean;
ElSubMenu, tooltipDelay?: number;
ElMenuItem, popperClass?: string;
N8nIcon, mode?: 'router' | 'tabs';
N8nTooltip, activeTab?: string;
ConditionalRouterLink, handleSelect?: (item: IMenuItem) => void;
}, }
props: {
item: { const props = withDefaults(defineProps<MenuItemProps>(), {
type: Object as PropType<IMenuItem>, compact: false,
required: true, tooltipDelay: 300,
}, popperClass: '',
compact: { mode: 'router',
type: Boolean,
default: false,
},
tooltipDelay: {
type: Number,
default: 300,
},
popperClass: {
type: String,
default: '',
},
mode: {
type: String,
default: 'router',
validator: (value: string): boolean => ['router', 'tabs'].includes(value),
},
activeTab: {
type: String,
default: undefined,
},
handleSelect: {
type: Function as PropType<(item: IMenuItem) => void>,
default: undefined,
},
},
computed: {
availableChildren(): IMenuItem[] {
return Array.isArray(this.item.children)
? this.item.children.filter((child) => child.available !== false)
: [];
},
currentRoute(): RouteObject {
return (
this.$route || {
name: '',
path: '',
}
);
},
submenuPopperClass(): string {
const popperClass = [this.$style.submenuPopper, this.popperClass];
if (this.compact) {
popperClass.push(this.$style.compact);
}
return popperClass.join(' ');
},
},
methods: {
isItemActive(item: IMenuItem): boolean {
const isItemActive = this.isActive(item);
const hasActiveChild =
Array.isArray(item.children) && item.children.some((child) => this.isActive(child));
return isItemActive || hasActiveChild;
},
isActive(item: IMenuItem): boolean {
if (this.mode === 'router') {
return doesMenuItemMatchCurrentRoute(item, this.currentRoute);
} else {
return item.id === this.activeTab;
}
},
},
}); });
const $style = useCssModule();
const $route = useRoute();
const availableChildren = computed((): IMenuItem[] =>
Array.isArray(props.item.children)
? props.item.children.filter((child) => child.available !== false)
: [],
);
const currentRoute = computed(() => {
return $route ?? { name: '', path: '' };
});
const submenuPopperClass = computed((): string => {
const popperClass = [$style.submenuPopper, props.popperClass];
if (props.compact) {
popperClass.push($style.compact);
}
return popperClass.join(' ');
});
const isActive = (item: IMenuItem): boolean => {
if (props.mode === 'router') {
return doesMenuItemMatchCurrentRoute(item, currentRoute.value);
} else {
return item.id === props.activeTab;
}
};
const isItemActive = (item: IMenuItem): boolean => {
const hasActiveChild =
Array.isArray(item.children) && item.children.some((child) => isActive(child));
return isActive(item) || hasActiveChild;
};
</script> </script>
<style module lang="scss"> <style module lang="scss">

View file

@ -1,10 +1,13 @@
import type { IMenuItem, RouteObject } from '@/types'; import type { IMenuItem } from '@/types';
import type { RouteLocationRaw } from 'vue-router'; import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from 'vue-router';
/** /**
* Checks if the given menu item matches the current route. * Checks if the given menu item matches the current route.
*/ */
export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: RouteObject) { export function doesMenuItemMatchCurrentRoute(
item: IMenuItem,
currentRoute: RouteLocationNormalizedLoaded,
) {
let activateOnRouteNames: string[] = []; let activateOnRouteNames: string[] = [];
if (Array.isArray(item.activateOnRouteNames)) { if (Array.isArray(item.activateOnRouteNames)) {
activateOnRouteNames = item.activateOnRouteNames; activateOnRouteNames = item.activateOnRouteNames;
@ -20,7 +23,7 @@ export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: Rou
} }
return ( return (
activateOnRouteNames.includes(currentRoute.name ?? '') || activateOnRouteNames.includes((currentRoute.name as string) ?? '') ||
activateOnRoutePaths.includes(currentRoute.path) activateOnRoutePaths.includes(currentRoute.path)
); );
} }

View file

@ -35,99 +35,75 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { defineComponent, type PropType } from 'vue';
import type { Placement } from 'element-plus'; import type { Placement } from 'element-plus';
import N8nTooltip from '../N8nTooltip'; import N8nTooltip from '../N8nTooltip';
export default defineComponent({ interface NodeIconProps {
name: 'N8nNodeIcon', type: 'file' | 'icon' | 'unknown';
components: { src?: string;
N8nTooltip, name?: string;
FontAwesomeIcon, nodeTypeName?: string;
}, size?: number;
props: { disabled?: boolean;
type: { circle?: boolean;
type: String, color?: string;
required: true, showTooltip?: boolean;
validator: (value: string): boolean => ['file', 'icon', 'unknown'].includes(value), tooltipPosition?: Placement;
}, badge?: { src: string; type: string };
src: { }
type: String,
},
name: {
type: String,
},
nodeTypeName: {
type: String,
},
size: {
type: Number,
},
disabled: {
type: Boolean,
},
circle: {
type: Boolean,
},
color: {
type: String,
},
showTooltip: {
type: Boolean,
},
tooltipPosition: {
type: String as PropType<Placement>,
default: 'top',
},
badge: { type: Object as PropType<{ src: string; type: string }> },
},
computed: {
iconStyleData(): Record<string, string> {
if (!this.size) {
return {
color: this.color || '',
};
}
return { const props = withDefaults(defineProps<NodeIconProps>(), {
color: this.color || '', tooltipPosition: 'top',
width: `${this.size}px`, });
height: `${this.size}px`,
'font-size': `${this.size}px`,
'line-height': `${this.size}px`,
};
},
badgeSize(): number {
switch (this.size) {
case 40:
return 18;
case 24:
return 10;
case 18:
default:
return 8;
}
},
badgeStyleData(): Record<string, string> {
const size = this.badgeSize;
return {
padding: `${Math.floor(size / 4)}px`,
right: `-${Math.floor(size / 2)}px`,
bottom: `-${Math.floor(size / 2)}px`,
};
},
fontStyleData(): Record<string, string> {
if (!this.size) {
return {};
}
return { const iconStyleData = computed((): Record<string, string> => {
'max-width': `${this.size}px`, if (!props.size) {
}; return {
}, color: props.color || '',
}, };
}
return {
color: props.color || '',
width: `${props.size}px`,
height: `${props.size}px`,
'font-size': `${props.size}px`,
'line-height': `${props.size}px`,
};
});
const badgeSize = computed((): number => {
switch (props.size) {
case 40:
return 18;
case 24:
return 10;
case 18:
default:
return 8;
}
});
const fontStyleData = computed((): Record<string, string> => {
if (!props.size) {
return {};
}
return {
'max-width': `${props.size}px`,
};
});
const badgeStyleData = computed((): Record<string, string> => {
const size = badgeSize.value;
return {
padding: `${Math.floor(size / 4)}px`,
right: `-${Math.floor(size / 2)}px`,
bottom: `-${Math.floor(size / 2)}px`,
};
}); });
</script> </script>

View file

@ -58,18 +58,17 @@ Sanitized.args = {
'<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.', '<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
}; };
export const Truncated = PropTemplate.bind({}); export const FullContent = PropTemplate.bind({});
Truncated.args = { FullContent.args = {
theme: 'warning', theme: 'warning',
truncate: true, content: 'This is just the summary. <a data-key="toggle-expand">Show more</a>',
content: fullContent:
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod ut labore et dolore magna aliqua. <a data-key="show-less">Show less</a>',
}; };
export const HtmlEdgeCase = PropTemplate.bind({}); export const HtmlEdgeCase = PropTemplate.bind({});
HtmlEdgeCase.args = { HtmlEdgeCase.args = {
theme: 'warning', theme: 'warning',
truncate: true,
content: content:
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.', 'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
}; };

View file

@ -7,7 +7,7 @@
:id="`${id}-content`" :id="`${id}-content`"
:class="showFullContent ? $style['expanded'] : $style['truncated']" :class="showFullContent ? $style['expanded'] : $style['truncated']"
role="region" role="region"
v-html="sanitizeHtml(showFullContent ? fullContent : content)" v-html="displayContent"
/> />
</slot> </slot>
</N8nText> </N8nText>
@ -15,89 +15,70 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed, ref, useCssModule } from 'vue';
import sanitizeHtml from 'sanitize-html'; import sanitize from 'sanitize-html';
import N8nText from '../../components/N8nText'; import N8nText from '../../components/N8nText';
import Locale from '../../mixins/locale';
import { uid } from '../../utils'; import { uid } from '../../utils';
export default defineComponent({ interface NoticeProps {
name: 'N8nNotice', id?: string;
directives: {}, theme?: 'success' | 'warning' | 'danger' | 'info';
components: { content?: string;
N8nText, fullContent?: string;
}, }
mixins: [Locale],
props: {
id: {
type: String,
default: () => uid('notice'),
},
theme: {
type: String,
default: 'warning',
},
content: {
type: String,
default: '',
},
fullContent: {
type: String,
default: '',
},
},
data() {
return {
showFullContent: false,
};
},
computed: {
classes(): string[] {
return ['notice', this.$style.notice, this.$style[this.theme]];
},
canTruncate(): boolean {
return this.fullContent !== undefined;
},
},
methods: {
toggleExpanded() {
this.showFullContent = !this.showFullContent;
},
sanitizeHtml(text: string): string {
return sanitizeHtml(text, {
allowedAttributes: {
a: [
'data-key',
'href',
'target',
'data-action',
'data-action-parameter-connectiontype',
'data-action-parameter-creatorview',
],
},
});
},
onClick(event: MouseEvent) {
if (!(event.target instanceof HTMLElement)) return;
if (event.target.localName !== 'a') return; const props = withDefaults(defineProps<NoticeProps>(), {
id: () => uid('notice'),
if (event.target.dataset?.key) { theme: 'warning',
event.stopPropagation(); content: '',
event.preventDefault(); fullContent: '',
if (event.target.dataset.key === 'show-less') {
this.showFullContent = false;
} else if (this.canTruncate && event.target.dataset.key === 'toggle-expand') {
this.showFullContent = !this.showFullContent;
} else {
this.$emit('action', event.target.dataset.key);
}
}
},
},
}); });
const $emit = defineEmits<{
(event: 'action', key: string): void;
}>();
const $style = useCssModule();
const classes = computed(() => ['notice', $style.notice, $style[props.theme]]);
const canTruncate = computed(() => props.fullContent !== undefined);
const showFullContent = ref(false);
const displayContent = computed(() =>
sanitize(showFullContent.value ? props.fullContent : props.content, {
allowedAttributes: {
a: [
'data-key',
'href',
'target',
'data-action',
'data-action-parameter-connectiontype',
'data-action-parameter-creatorview',
],
},
}),
);
const onClick = (event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) return;
if (event.target.localName !== 'a') return;
const anchorKey = event.target.dataset?.key;
if (anchorKey) {
event.stopPropagation();
event.preventDefault();
if (anchorKey === 'show-less') {
showFullContent.value = false;
} else if (canTruncate.value && anchorKey === 'toggle-expand') {
showFullContent.value = !showFullContent.value;
} else {
$emit('action', anchorKey);
}
}
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -8,12 +8,8 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import { defineComponent } from 'vue'; defineOptions({ name: 'N8nPulse' });
export default defineComponent({
name: 'N8nPulse',
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -7,7 +7,7 @@
[$style.container]: true, [$style.container]: true,
[$style.hoverable]: !disabled, [$style.hoverable]: !disabled,
}" }"
aria-checked="true" :aria-checked="active"
> >
<div <div
:class="{ :class="{
@ -23,33 +23,19 @@
</label> </label>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; interface RadioButtonProps {
label: string;
value: string;
active?: boolean;
disabled?: boolean;
size?: 'small' | 'medium';
}
export default defineComponent({ withDefaults(defineProps<RadioButtonProps>(), {
name: 'N8nRadioButton', active: false,
props: { disabled: false,
label: { size: 'medium',
type: String,
required: true,
},
value: {
type: String,
required: true,
},
active: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},
disabled: {
type: Boolean,
},
},
}); });
</script> </script>

View file

@ -54,3 +54,19 @@ Example.args = {
}, },
], ],
}; };
export const Disabled = Template.bind({});
Disabled.args = {
modelValue: 'enabled',
options: [
{
label: 'Enabled',
value: 'enabled',
},
{
label: 'Disabled',
value: 'disabled',
disabled: true,
},
],
};

View file

@ -15,48 +15,41 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import RadioButton from './RadioButton.vue'; import RadioButton from './RadioButton.vue';
import type { PropType } from 'vue'; interface RadioOption {
import { defineComponent } from 'vue';
export interface RadioOption {
label: string; label: string;
value: string; value: string;
disabled?: boolean; disabled?: boolean;
} }
export default defineComponent({ interface RadioButtonsProps {
name: 'N8nRadioButtons', modelValue?: string;
components: { options?: RadioOption[];
RadioButton, size: 'small' | 'medium';
}, disabled?: boolean;
props: { }
modelValue: {
type: String, const props = withDefaults(defineProps<RadioButtonsProps>(), {
}, active: false,
options: { disabled: false,
type: Array as PropType<RadioOption[]>, size: 'medium',
default: (): RadioOption[] => [],
},
size: {
type: String,
},
disabled: {
type: Boolean,
},
},
emits: ['update:modelValue'],
methods: {
onClick(option: { label: string; value: string; disabled?: boolean }, event: MouseEvent) {
if (this.disabled || option.disabled) {
return;
}
this.$emit('update:modelValue', option.value, event);
},
},
}); });
const $emit = defineEmits<{
(event: 'update:modelValue', value: string, e: MouseEvent): void;
}>();
const onClick = (
option: { label: string; value: string; disabled?: boolean },
event: MouseEvent,
) => {
if (props.disabled || option.disabled) {
return;
}
$emit('update:modelValue', option.value, event);
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -1,6 +1,6 @@
import type { ComponentInstance } from 'vue';
import type { StoryFn } from '@storybook/vue3'; import type { StoryFn } from '@storybook/vue3';
import N8nRecycleScroller from './RecycleScroller.vue'; import N8nRecycleScroller from './RecycleScroller.vue';
import type { ComponentInstance } from 'vue';
export default { export default {
title: 'Atoms/RecycleScroller', title: 'Atoms/RecycleScroller',
@ -23,7 +23,9 @@ const Template: StoryFn = (args) => ({
}, },
methods: { methods: {
resizeItem(item: { id: string; height: string }, fn: (item: { id: string }) => void) { resizeItem(item: { id: string; height: string }, fn: (item: { id: string }) => void) {
const itemRef = (this as ComponentInstance).$refs[`item-${item.id}`] as HTMLElement; const itemRef = (this as ComponentInstance<typeof N8nRecycleScroller>).$refs[
`item-${item.id}`
] as HTMLElement;
item.height = '200px'; item.height = '200px';
itemRef.style.height = '200px'; itemRef.style.height = '200px';

View file

@ -1,225 +1,182 @@
<script lang="ts"> <script lang="ts" setup>
/* eslint-disable @typescript-eslint/no-use-before-define */ import type { ComponentPublicInstance } from 'vue';
import { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
import type { PropType, ComponentPublicInstance } from 'vue'; interface RecycleScrollerProps {
import { computed, defineComponent, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue'; itemSize: number;
items: Array<Record<string, string>>;
itemKey: string;
offset?: number;
}
export default defineComponent({ const props = withDefaults(defineProps<RecycleScrollerProps>(), {
name: 'N8nRecycleScroller', offset: 2,
props: { });
itemSize: {
type: Number,
required: true,
},
items: {
type: Array as PropType<Array<Record<string, string>>>,
required: true,
},
itemKey: {
type: String,
required: true,
},
offset: {
type: Number,
default: 2,
},
},
setup(props) {
const wrapperRef = ref<HTMLElement | null>(null);
const scrollerRef = ref<HTMLElement | null>(null);
const itemsRef = ref<HTMLElement | null>(null);
const itemRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({});
const scrollTop = ref(0); const wrapperRef = ref<HTMLElement | null>(null);
const wrapperHeight = ref(0); const scrollerRef = ref<HTMLElement | null>(null);
const windowHeight = ref(0); const itemsRef = ref<HTMLElement | null>(null);
const itemRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({});
const itemCount = computed(() => props.items.length); const scrollTop = ref(0);
const wrapperHeight = ref(0);
const windowHeight = ref(0);
/** /** Cache */
* Cache
*/
const itemSizeCache = ref<Record<string, number>>({}); const itemSizeCache = ref<Record<string, number>>({});
const itemPositionCache = computed(() => { const itemPositionCache = computed(() => {
return props.items.reduce<Record<string, number>>((acc, item, index) => { return props.items.reduce<Record<string, number>>((acc, item, index) => {
const key = item[props.itemKey]; const key = item[props.itemKey];
const prevItem = props.items[index - 1]; const prevItem = props.items[index - 1];
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0; const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0; const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
acc[key] = prevItemPosition + prevItemSize; acc[key] = prevItemPosition + prevItemSize;
return acc; return acc;
}, {}); }, {});
}); });
/** /** Indexes */
* Indexes
*/
const startIndex = computed(() => { const startIndex = computed(() => {
const foundIndex = const foundIndex =
props.items.findIndex((item) => { props.items.findIndex((item) => {
const itemPosition = itemPositionCache.value[item[props.itemKey]]; const itemPosition = itemPositionCache.value[item[props.itemKey]];
return itemPosition >= scrollTop.value; return itemPosition >= scrollTop.value;
}) - 1; }) - 1;
const index = foundIndex - props.offset; const index = foundIndex - props.offset;
return index < 0 ? 0 : index; return index < 0 ? 0 : index;
}); });
const endIndex = computed(() => { const endIndex = computed(() => {
const foundIndex = props.items.findIndex((item) => { const foundIndex = props.items.findIndex((item) => {
const itemPosition = itemPositionCache.value[item[props.itemKey]]; const itemPosition = itemPositionCache.value[item[props.itemKey]];
const itemSize = itemSizeCache.value[item[props.itemKey]]; const itemSize = itemSizeCache.value[item[props.itemKey]];
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value; return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
}); });
const index = foundIndex + props.offset; const index = foundIndex + props.offset;
return foundIndex === -1 ? props.items.length - 1 : index; return foundIndex === -1 ? props.items.length - 1 : index;
}); });
const visibleItems = computed(() => { const visibleItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.value + 1); return props.items.slice(startIndex.value, endIndex.value + 1);
}); });
watch( watch(
() => visibleItems.value, () => visibleItems.value,
(currentValue, previousValue) => { (currentValue, previousValue) => {
const difference = currentValue.filter( const difference = currentValue.filter(
(currentItem) => (currentItem) =>
!previousValue.find( !previousValue.find(
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey], (previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
), ),
);
if (difference.length > 0) {
updateItemSizeCache(difference);
}
},
); );
/** if (difference.length > 0) {
* Computed sizes and styles updateItemSizeCache(difference);
*/
const scrollerHeight = computed(() => {
const lastItem = props.items[props.items.length - 1];
const lastItemPosition = lastItem ? itemPositionCache.value[lastItem[props.itemKey]] : 0;
const lastItemSize = lastItem ? itemSizeCache.value[lastItem[props.itemKey]] : props.itemSize;
return lastItemPosition + lastItemSize;
});
const scrollerStyles = computed(() => ({
height: `${scrollerHeight.value}px`,
}));
const itemsStyles = computed(() => {
const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
return {
transform: `translateY(${offset}px)`,
};
});
/**
* Lifecycle hooks
*/
onBeforeMount(() => {
initializeItemSizeCache();
});
onMounted(() => {
if (wrapperRef.value) {
wrapperRef.value.addEventListener('scroll', onScroll);
updateItemSizeCache(visibleItems.value);
}
window.addEventListener('resize', onWindowResize);
onWindowResize();
});
/**
* Event handlers
*/
function initializeItemSizeCache() {
props.items.forEach((item) => {
itemSizeCache.value = {
...itemSizeCache.value,
[item[props.itemKey]]: props.itemSize,
};
});
} }
},
);
function updateItemSizeCache(items: Array<Record<string, string>>) { /** Computed sizes and styles */
for (const item of items) {
onUpdateItemSize(item);
}
}
function onUpdateItemSize(item: { [key: string]: string }) { const scrollerHeight = computed(() => {
void nextTick(() => { const lastItem = props.items[props.items.length - 1];
const itemId = item[props.itemKey]; const lastItemPosition = lastItem ? itemPositionCache.value[lastItem[props.itemKey]] : 0;
const itemRef = itemRefs.value[itemId] as HTMLElement; const lastItemSize = lastItem ? itemSizeCache.value[lastItem[props.itemKey]] : props.itemSize;
const previousSize = itemSizeCache.value[itemId];
const size = itemRef ? itemRef.offsetHeight : props.itemSize;
const difference = size - previousSize;
itemSizeCache.value = { return lastItemPosition + lastItemSize;
...itemSizeCache.value, });
[item[props.itemKey]]: size,
};
if (wrapperRef.value && scrollTop.value) { const scrollerStyles = computed(() => ({
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference; height: `${scrollerHeight.value}px`,
scrollTop.value = wrapperRef.value.scrollTop; }));
}
});
}
function onWindowResize() { const itemsStyles = computed(() => {
if (wrapperRef.value) { const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
wrapperHeight.value = wrapperRef.value.offsetHeight;
void nextTick(() => {
updateItemSizeCache(visibleItems.value);
});
}
windowHeight.value = window.innerHeight; return {
} transform: `translateY(${offset}px)`,
};
});
function onScroll() { /** Lifecycle hooks */
if (!wrapperRef.value) {
return;
}
onBeforeMount(() => {
initializeItemSizeCache();
});
onMounted(() => {
if (wrapperRef.value) {
wrapperRef.value.addEventListener('scroll', onScroll);
updateItemSizeCache(visibleItems.value);
}
window.addEventListener('resize', onWindowResize);
onWindowResize();
});
/** Event handlers */
function initializeItemSizeCache() {
props.items.forEach((item) => {
itemSizeCache.value = {
...itemSizeCache.value,
[item[props.itemKey]]: props.itemSize,
};
});
}
function updateItemSizeCache(items: Array<Record<string, string>>) {
for (const item of items) {
onUpdateItemSize(item);
}
}
function onUpdateItemSize(item: { [key: string]: string }) {
void nextTick(() => {
const itemId = item[props.itemKey];
const itemRef = itemRefs.value[itemId] as HTMLElement;
const previousSize = itemSizeCache.value[itemId];
const size = itemRef ? itemRef.offsetHeight : props.itemSize;
const difference = size - previousSize;
itemSizeCache.value = {
...itemSizeCache.value,
[item[props.itemKey]]: size,
};
if (wrapperRef.value && scrollTop.value) {
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference;
scrollTop.value = wrapperRef.value.scrollTop; scrollTop.value = wrapperRef.value.scrollTop;
} }
});
}
return { function onWindowResize() {
startIndex, if (wrapperRef.value) {
endIndex, wrapperHeight.value = wrapperRef.value.offsetHeight;
itemCount, void nextTick(() => {
itemSizeCache, updateItemSizeCache(visibleItems.value);
itemPositionCache, });
itemsVisible: visibleItems, }
itemsStyles,
scrollerStyles, windowHeight.value = window.innerHeight;
scrollerScrollTop: scrollTop, }
scrollerRef,
wrapperRef, function onScroll() {
itemsRef, if (!wrapperRef.value) {
itemRefs, return;
onUpdateItemSize, }
};
}, scrollTop.value = wrapperRef.value.scrollTop;
}); }
</script> </script>
<template> <template>
@ -227,7 +184,7 @@ export default defineComponent({
<div ref="scrollerRef" class="recycle-scroller" :style="scrollerStyles"> <div ref="scrollerRef" class="recycle-scroller" :style="scrollerStyles">
<div ref="itemsRef" class="recycle-scroller-items-wrapper" :style="itemsStyles"> <div ref="itemsRef" class="recycle-scroller-items-wrapper" :style="itemsStyles">
<div <div
v-for="item in itemsVisible" v-for="item in visibleItems"
:key="item[itemKey]" :key="item[itemKey]"
:ref="(element) => (itemRefs[item[itemKey]] = element)" :ref="(element) => (itemRefs[item[itemKey]] = element)"
class="recycle-scroller-item" class="recycle-scroller-item"

View file

@ -3,7 +3,7 @@ import N8nRecycleScroller from '../RecycleScroller.vue';
const itemSize = 100; const itemSize = 100;
const itemKey = 'id'; const itemKey = 'id';
const items = [...(new Array(100) as number[])].map((item, index) => ({ const items = [...(new Array(100) as number[])].map((_, index) => ({
id: index, id: index,
name: `Item ${index}`, name: `Item ${index}`,
})); }));

View file

@ -11,9 +11,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import { computed, ref } from 'vue';
import { defineComponent } from 'vue';
function closestNumber(value: number, divisor: number): number { function closestNumber(value: number, divisor: number): number {
const q = value / divisor; const q = value / divisor;
@ -35,7 +34,7 @@ function getSize(min: number, virtual: number, gridSize: number): number {
return min; return min;
} }
const directionsCursorMaps: { [key: string]: string } = { const directionsCursorMaps = {
right: 'ew-resize', right: 'ew-resize',
top: 'ns-resize', top: 'ns-resize',
bottom: 'ns-resize', bottom: 'ns-resize',
@ -44,141 +43,141 @@ const directionsCursorMaps: { [key: string]: string } = {
topRight: 'ne-resize', topRight: 'ne-resize',
bottomLeft: 'sw-resize', bottomLeft: 'sw-resize',
bottomRight: 'se-resize', bottomRight: 'se-resize',
} as const;
type Direction = keyof typeof directionsCursorMaps;
interface ResizeProps {
isResizingEnabled?: boolean;
height?: number;
width?: number;
minHeight?: number;
minWidth?: number;
scale?: number;
gridSize?: number;
supportedDirections?: Direction[];
}
const props = withDefaults(defineProps<ResizeProps>(), {
isResizingEnabled: true,
height: 0,
width: 0,
minHeight: 0,
minWidth: 0,
scale: 1,
gridSize: 20,
supportedDirections: () => [],
});
export interface ResizeData {
height: number;
width: number;
dX: number;
dY: number;
x: number;
y: number;
direction: Direction;
}
const $emit = defineEmits<{
(event: 'resizestart');
(event: 'resize', value: ResizeData): void;
(event: 'resizeend');
}>();
const enabledDirections = computed((): Direction[] => {
const availableDirections = Object.keys(directionsCursorMaps) as Direction[];
if (!props.isResizingEnabled) return [];
if (props.supportedDirections.length === 0) return availableDirections;
return props.supportedDirections;
});
const state = {
dir: ref<Direction | ''>(''),
dHeight: ref(0),
dWidth: ref(0),
vHeight: ref(0),
vWidth: ref(0),
x: ref(0),
y: ref(0),
}; };
export default defineComponent({ const mouseMove = (event: MouseEvent) => {
name: 'N8nResize', event.preventDefault();
props: { event.stopPropagation();
isResizingEnabled: { let dWidth = 0;
type: Boolean, let dHeight = 0;
default: true, let top = false;
}, let left = false;
height: {
type: Number,
default: 0,
},
width: {
type: Number,
default: 0,
},
minHeight: {
type: Number,
default: 0,
},
minWidth: {
type: Number,
default: 0,
},
scale: {
type: Number,
default: 1,
},
gridSize: {
type: Number,
default: 20,
},
supportedDirections: {
type: Array as PropType<string[]>,
default: (): string[] => [],
},
},
data() {
return {
directionsCursorMaps,
dir: '',
dHeight: 0,
dWidth: 0,
vHeight: 0,
vWidth: 0,
x: 0,
y: 0,
};
},
computed: {
enabledDirections(): string[] {
const availableDirections = Object.keys(directionsCursorMaps);
if (!this.isResizingEnabled) return []; if (state.dir.value.includes('right')) {
if (this.supportedDirections.length === 0) return availableDirections; dWidth = event.pageX - state.x.value;
}
if (state.dir.value.includes('left')) {
dWidth = state.x.value - event.pageX;
left = true;
}
if (state.dir.value.includes('top')) {
dHeight = state.y.value - event.pageY;
top = true;
}
if (state.dir.value.includes('bottom')) {
dHeight = event.pageY - state.y.value;
}
return this.supportedDirections; const deltaWidth = (dWidth - state.dWidth.value) / props.scale;
}, const deltaHeight = (dHeight - state.dHeight.value) / props.scale;
},
methods: {
resizerMove(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const targetResizer = event.target as { dataset: { dir: string } } | null; state.vHeight.value = state.vHeight.value + deltaHeight;
if (targetResizer) { state.vWidth.value = state.vWidth.value + deltaWidth;
this.dir = targetResizer.dataset.dir.toLocaleLowerCase(); const height = getSize(props.minHeight, state.vHeight.value, props.gridSize);
} const width = getSize(props.minWidth, state.vWidth.value, props.gridSize);
document.body.style.cursor = directionsCursorMaps[this.dir]; const dX = left && width !== props.width ? -1 * (width - props.width) : 0;
const dY = top && height !== props.height ? -1 * (height - props.height) : 0;
const x = event.x;
const y = event.y;
const direction = state.dir.value as Direction;
this.x = event.pageX; $emit('resize', { height, width, dX, dY, x, y, direction });
this.y = event.pageY; state.dHeight.value = dHeight;
this.dWidth = 0; state.dWidth.value = dWidth;
this.dHeight = 0; };
this.vHeight = this.height;
this.vWidth = this.width;
window.addEventListener('mousemove', this.mouseMove); const mouseUp = (event: MouseEvent) => {
window.addEventListener('mouseup', this.mouseUp); event.preventDefault();
this.$emit('resizestart'); event.stopPropagation();
}, $emit('resizeend');
mouseMove(event: MouseEvent) { window.removeEventListener('mousemove', mouseMove);
event.preventDefault(); window.removeEventListener('mouseup', mouseUp);
event.stopPropagation(); document.body.style.cursor = 'unset';
let dWidth = 0; state.dir.value = '';
let dHeight = 0; };
let top = false;
let left = false;
if (this.dir.includes('right')) { const resizerMove = (event: MouseEvent) => {
dWidth = event.pageX - this.x; event.preventDefault();
} event.stopPropagation();
if (this.dir.includes('left')) {
dWidth = this.x - event.pageX;
left = true;
}
if (this.dir.includes('top')) {
dHeight = this.y - event.pageY;
top = true;
}
if (this.dir.includes('bottom')) {
dHeight = event.pageY - this.y;
}
const deltaWidth = (dWidth - this.dWidth) / this.scale; const targetResizer = event.target as { dataset: { dir: string } } | null;
const deltaHeight = (dHeight - this.dHeight) / this.scale; if (targetResizer) {
state.dir.value = targetResizer.dataset.dir.toLocaleLowerCase() as Direction;
}
this.vHeight = this.vHeight + deltaHeight; document.body.style.cursor = directionsCursorMaps[state.dir.value];
this.vWidth = this.vWidth + deltaWidth;
const height = getSize(this.minHeight, this.vHeight, this.gridSize);
const width = getSize(this.minWidth, this.vWidth, this.gridSize);
const dX = left && width !== this.width ? -1 * (width - this.width) : 0; state.x.value = event.pageX;
const dY = top && height !== this.height ? -1 * (height - this.height) : 0; state.y.value = event.pageY;
const x = event.x; state.dWidth.value = 0;
const y = event.y; state.dHeight.value = 0;
const direction = this.dir; state.vHeight.value = props.height;
state.vWidth.value = props.width;
this.$emit('resize', { height, width, dX, dY, x, y, direction }); window.addEventListener('mousemove', mouseMove);
this.dHeight = dHeight; window.addEventListener('mouseup', mouseUp);
this.dWidth = dWidth; $emit('resizestart');
}, };
mouseUp(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.$emit('resizeend');
window.removeEventListener('mousemove', this.mouseMove);
window.removeEventListener('mouseup', this.mouseUp);
document.body.style.cursor = 'unset';
this.dir = '';
},
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -9,12 +9,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { type RouteLocationRaw } from 'vue-router';
// TODO: replace `object` with a more detailed type
export type RouteTo = string | object;
interface RouteProps { interface RouteProps {
to?: RouteTo; to?: RouteLocationRaw;
newWindow?: boolean; newWindow?: boolean;
} }

View file

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

View file

@ -30,16 +30,11 @@
<script lang="ts"> <script lang="ts">
import { ElSelect } from 'element-plus'; import { ElSelect } from 'element-plus';
import { defineComponent } from 'vue'; import { type PropType, defineComponent } from 'vue';
import type { SelectSize } from '@/types';
type InnerSelectRef = InstanceType<typeof ElSelect>; type InnerSelectRef = InstanceType<typeof ElSelect>;
export interface IProps {
size?: string;
limitPopperWidth?: string;
popperClass?: string;
}
export default defineComponent({ export default defineComponent({
name: 'N8nSelect', name: 'N8nSelect',
components: { components: {
@ -49,10 +44,8 @@ export default defineComponent({
...ElSelect.props, ...ElSelect.props,
modelValue: {}, modelValue: {},
size: { size: {
type: String, type: String as PropType<SelectSize>,
default: 'large', default: 'large',
validator: (value: string): boolean =>
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
}, },
placeholder: { placeholder: {
type: String, type: String,
@ -101,7 +94,7 @@ export default defineComponent({
return acc; return acc;
}, {}); }, {});
}, },
computedSize(): string | undefined { computedSize(): InnerSelectRef['$props']['size'] {
if (this.size === 'medium') { if (this.size === 'medium') {
return 'default'; return 'default';
} }

View file

@ -57,150 +57,120 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
import N8nMarkdown from '../N8nMarkdown'; import N8nMarkdown from '../N8nMarkdown';
import N8nResizeWrapper from '../N8nResizeWrapper'; import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import Locale from '../../mixins/locale'; import { useI18n } from '../../composables/useI18n';
import { defineComponent } from 'vue';
export default defineComponent({ interface StickyProps {
name: 'N8nSticky', modelValue?: string;
components: { height?: number;
N8nInput, width?: number;
N8nMarkdown, minHeight?: number;
N8nResizeWrapper, minWidth?: number;
N8nText, scale?: number;
}, gridSize?: number;
mixins: [Locale], id?: string;
props: { defaultText?: string;
modelValue: { editMode?: boolean;
type: String, readOnly?: boolean;
}, backgroundColor?: number | string;
height: { }
type: Number,
default: 180,
},
width: {
type: Number,
default: 240,
},
minHeight: {
type: Number,
default: 80,
},
minWidth: {
type: Number,
default: 150,
},
scale: {
type: Number,
default: 1,
},
gridSize: {
type: Number,
default: 20,
},
id: {
type: String,
default: '0',
},
defaultText: {
type: String,
},
editMode: {
type: Boolean,
default: false,
},
readOnly: {
type: Boolean,
default: false,
},
backgroundColor: {
value: [Number, String],
default: 1,
},
},
data() {
return {
isResizing: false,
};
},
computed: {
resHeight(): number {
if (this.height < this.minHeight) {
return this.minHeight;
}
return this.height;
},
resWidth(): number {
if (this.width < this.minWidth) {
return this.minWidth;
}
return this.width;
},
styles(): { height: string; width: string } {
const styles: { height: string; width: string } = {
height: `${this.resHeight}px`,
width: `${this.resWidth}px`,
};
return styles; const props = withDefaults(defineProps<StickyProps>(), {
}, height: 180,
shouldShowFooter(): boolean { width: 240,
return this.resHeight > 100 && this.resWidth > 155; minHeight: 80,
}, minWidth: 150,
}, scale: 1,
watch: { gridSize: 20,
editMode(newMode, prevMode) { id: '0',
setTimeout(() => { editMode: false,
if (newMode && !prevMode && this.$refs.input) { readOnly: false,
const textarea = this.$refs.input as HTMLTextAreaElement; backgroundColor: 1,
if (this.defaultText === this.modelValue) {
textarea.select();
}
textarea.focus();
}
}, 100);
},
},
methods: {
onDoubleClick() {
if (!this.readOnly) {
this.$emit('edit', true);
}
},
onInputBlur() {
if (!this.isResizing) {
this.$emit('edit', false);
}
},
onUpdateModelValue(value: string) {
this.$emit('update:modelValue', value);
},
onMarkdownClick(link: string, event: Event) {
this.$emit('markdown-click', link, event);
},
onResize(values: unknown[]) {
this.$emit('resize', values);
},
onResizeEnd(resizeEnd: unknown) {
this.isResizing = false;
this.$emit('resizeend', resizeEnd);
},
onResizeStart() {
this.isResizing = true;
this.$emit('resizestart');
},
onInputScroll(event: WheelEvent) {
// Pass through zoom events but hold regular scrolling
if (!event.ctrlKey && !event.metaKey) {
event.stopPropagation();
}
},
},
}); });
const $emit = defineEmits<{
(event: 'edit', editing: boolean);
(event: 'update:modelValue', value: string);
(event: 'markdown-click', link: string, e: Event);
(event: 'resize', values: ResizeData);
(event: 'resizestart');
(event: 'resizeend', value: unknown);
}>();
const { t } = useI18n();
const isResizing = ref(false);
const input = ref<HTMLTextAreaElement | undefined>(undefined);
const resHeight = computed((): number => {
return props.height < props.minHeight ? props.minHeight : props.height;
});
const resWidth = computed((): number => {
return props.width < props.minWidth ? props.minWidth : props.width;
});
const styles = computed((): { height: string; width: string } => ({
height: `${resHeight.value}px`,
width: `${resWidth.value}px`,
}));
const shouldShowFooter = computed((): boolean => resHeight.value > 100 && resWidth.value > 155);
watch(
() => props.editMode,
(newMode, prevMode) => {
setTimeout(() => {
if (newMode && !prevMode && input.value) {
if (props.defaultText === props.modelValue) {
input.value.select();
}
input.value.focus();
}
}, 100);
},
);
const onDoubleClick = () => {
if (!props.readOnly) $emit('edit', true);
};
const onInputBlur = () => {
if (!isResizing.value) $emit('edit', false);
};
const onUpdateModelValue = (value: string) => {
$emit('update:modelValue', value);
};
const onMarkdownClick = (link: string, event: Event) => {
$emit('markdown-click', link, event);
};
const onResize = (values: ResizeData) => {
$emit('resize', values);
};
const onResizeStart = () => {
isResizing.value = true;
$emit('resizestart');
};
const onResizeEnd = (resizeEnd: unknown) => {
isResizing.value = false;
$emit('resizeend', resizeEnd);
};
const onInputScroll = (event: WheelEvent) => {
// Pass through zoom events but hold regular scrolling
if (!event.ctrlKey && !event.metaKey) {
event.stopPropagation();
}
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -36,8 +36,12 @@ export const Example = Template.bind({});
Example.args = { Example.args = {
options: [ options: [
{ {
label: 'Test', label: 'First',
value: 'test', value: 'first',
},
{
label: 'Second',
value: 'second',
}, },
{ {
label: 'Github', label: 'Github',

View file

@ -47,12 +47,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { defineComponent } from 'vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
export interface N8nTabOptions { interface TabOptions {
value: string; value: string;
label?: string; label?: string;
icon?: string; icon?: string;
@ -61,85 +60,63 @@ export interface N8nTabOptions {
align?: 'left' | 'right'; align?: 'left' | 'right';
} }
export default defineComponent({ interface TabsProps {
name: 'N8nTabs', modelValue?: string;
components: { options?: TabOptions[];
N8nIcon, }
},
props: {
modelValue: {
type: String,
default: '',
},
options: {
type: Array as PropType<N8nTabOptions[]>,
default: (): N8nTabOptions[] => [],
},
},
data() {
return {
scrollPosition: 0,
canScrollRight: false,
resizeObserver: null as ResizeObserver | null,
};
},
mounted() {
const container = this.$refs.tabs as HTMLDivElement | undefined;
if (container) {
container.addEventListener('scroll', (event: Event) => {
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
this.scrollPosition = (event.target as Element).scrollLeft;
this.canScrollRight = scrollWidth - width > this.scrollPosition; withDefaults(defineProps<TabsProps>(), {
}); options: () => [],
this.resizeObserver = new ResizeObserver(() => {
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
this.canScrollRight = scrollWidth - width > this.scrollPosition;
});
this.resizeObserver.observe(container);
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
this.canScrollRight = scrollWidth - width > this.scrollPosition;
}
},
unmounted() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
},
methods: {
handleTooltipClick(tab: string, event: MouseEvent) {
this.$emit('tooltipClick', tab, event);
},
handleTabClick(tab: string) {
this.$emit('update:modelValue', tab);
},
scrollLeft() {
this.scroll(-50);
},
scrollRight() {
this.scroll(50);
},
scroll(left: number) {
const container = this.$refs.tabs as
| (HTMLDivElement & { scrollBy: ScrollByFunction })
| undefined;
if (container) {
container.scrollBy({ left, top: 0, behavior: 'smooth' });
}
},
},
}); });
type ScrollByFunction = (arg: { const scrollPosition = ref(0);
left: number; const canScrollRight = ref(false);
top: number; const tabs = ref<Element | undefined>(undefined);
behavior: 'smooth' | 'instant' | 'auto'; let resizeObserver: ResizeObserver | null = null;
}) => void;
onMounted(() => {
const container = tabs.value as Element;
if (container) {
container.addEventListener('scroll', (event: Event) => {
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
scrollPosition.value = (event.target as Element).scrollLeft;
canScrollRight.value = scrollWidth - width > scrollPosition.value;
});
resizeObserver = new ResizeObserver(() => {
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
canScrollRight.value = scrollWidth - width > scrollPosition.value;
});
resizeObserver.observe(container);
const width = container.clientWidth;
const scrollWidth = container.scrollWidth;
canScrollRight.value = scrollWidth - width > scrollPosition.value;
}
});
onUnmounted(() => {
resizeObserver?.disconnect();
});
const $emit = defineEmits<{
(event: 'tooltipClick', tab: string, e: MouseEvent): void;
(event: 'update:modelValue', tab: string);
}>();
const handleTooltipClick = (tab: string, event: MouseEvent) => $emit('tooltipClick', tab, event);
const handleTabClick = (tab: string) => $emit('update:modelValue', tab);
const scroll = (left: number) => {
const container = tabs.value;
if (container) {
container.scrollBy({ left, top: 0, behavior: 'smooth' });
}
};
const scrollLeft = () => scroll(-50);
const scrollRight = () => scroll(50);
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -19,7 +19,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, ref } from 'vue';
import N8nTag from '../N8nTag'; import N8nTag from '../N8nTag';
import N8nLink from '../N8nLink'; import N8nLink from '../N8nLink';
import { useI18n } from '../../composables/useI18n'; import { useI18n } from '../../composables/useI18n';
@ -46,11 +46,11 @@ const $emit = defineEmits(['expand', 'click:tag']);
const { t } = useI18n(); const { t } = useI18n();
let showAll = false; const showAll = ref(false);
const visibleTags = computed((): ITag[] => { const visibleTags = computed((): ITag[] => {
const { tags, truncate, truncateAt } = props; const { tags, truncate, truncateAt } = props;
if (truncate && !showAll && tags.length > truncateAt) { if (truncate && !showAll.value && tags.length > truncateAt) {
return tags.slice(0, truncateAt); return tags.slice(0, truncateAt);
} }
return tags; return tags;
@ -59,7 +59,7 @@ const visibleTags = computed((): ITag[] => {
const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt); const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt);
const onExpand = () => { const onExpand = () => {
showAll = true; showAll.value = true;
$emit('expand', true); $emit('expand', true);
}; };
</script> </script>

View file

@ -17,7 +17,7 @@
:value="value[label]" :value="value[label]"
:node-class="nodeClass" :node-class="nodeClass"
> >
<template v-for="(index, name) in $slots" #[name]="data"> <template v-for="(_, name) in $slots" #[name]="data">
<slot :name="name" v-bind="data"></slot> <slot :name="name" v-bind="data"></slot>
</template> </template>
</n8n-tree> </n8n-tree>
@ -26,60 +26,51 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import { computed, useCssModule } from 'vue';
import { defineComponent } from 'vue';
export default defineComponent({ interface TreeProps {
name: 'N8nTree', value?: Record<string, unknown>;
components: {}, path?: string[];
props: { depth?: number;
value: { nodeClass?: string;
type: Object as PropType<Record<string, unknown>>, }
default: () => ({}),
},
path: {
type: Array as PropType<string[]>,
default: () => [],
},
depth: {
type: Number,
default: 0,
},
nodeClass: {
type: String,
default: '',
},
},
computed: {
classes(): Record<string, boolean> {
return { [this.nodeClass]: !!this.nodeClass, [this.$style.indent]: this.depth > 0 };
},
},
methods: {
isSimple(data: unknown): boolean {
if (data === null || data === undefined) {
return true;
}
if (typeof data === 'object' && Object.keys(data).length === 0) { defineOptions({ name: 'N8nTree' });
return true; const props = withDefaults(defineProps<TreeProps>(), {
} value: () => ({}),
path: () => [],
if (Array.isArray(data) && data.length === 0) { depth: 0,
return true; nodeClass: '',
}
return typeof data !== 'object';
},
getPath(key: string): unknown[] {
if (Array.isArray(this.value)) {
return [...this.path, parseInt(key, 10)];
}
return [...this.path, key];
},
},
}); });
const $style = useCssModule();
const classes = computed((): Record<string, boolean> => {
return { [props.nodeClass]: !!props.nodeClass, [$style.indent]: props.depth > 0 };
});
const isSimple = (data: unknown): boolean => {
if (data === null || data === undefined) {
return true;
}
if (typeof data === 'object' && Object.keys(data).length === 0) {
return true;
}
if (Array.isArray(data) && data.length === 0) {
return true;
}
return typeof data !== 'object';
};
const getPath = (key: string): unknown[] => {
if (Array.isArray(props.value)) {
return [...props.path, parseInt(key, 10)];
}
return [...props.path, key];
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -5,7 +5,7 @@
:model-value="modelValue" :model-value="modelValue"
:filterable="true" :filterable="true"
:filter-method="setFilter" :filter-method="setFilter"
:placeholder="placeholder" :placeholder="placeholder || t('nds.userSelect.selectUser')"
:default-first-option="true" :default-first-option="true"
teleported teleported
:popper-class="$style.limitPopperWidth" :popper-class="$style.limitPopperWidth"
@ -30,114 +30,83 @@
</N8nSelect> </N8nSelect>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, ref } from 'vue';
import N8nUserInfo from '../N8nUserInfo'; import N8nUserInfo from '../N8nUserInfo';
import N8nSelect from '../N8nSelect'; import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption'; import N8nOption from '../N8nOption';
import type { IUser } from '../../types'; import { useI18n } from '../../composables/useI18n';
import Locale from '../../mixins/locale'; import type { IUser, SelectSize } from '../../types';
import { t } from '../../locale';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
export default defineComponent({ interface UserSelectProps {
name: 'N8nUserSelect', users?: IUser[];
components: { modelValue?: string;
N8nUserInfo, ignoreIds?: string[];
N8nSelect, currentUserId?: string;
N8nOption, placeholder?: string;
}, size?: Exclude<SelectSize, 'xlarge'>;
mixins: [Locale], }
props: {
users: {
type: Array as PropType<IUser[]>,
default: () => [],
},
modelValue: {
type: String,
default: '',
},
ignoreIds: {
type: Array as PropType<string[]>,
default: () => [],
validator: (ids: string[]) => !ids.find((id) => typeof id !== 'string'),
},
currentUserId: {
type: String,
default: '',
},
placeholder: {
type: String,
default: () => t('nds.userSelect.selectUser'),
},
size: {
type: String,
default: '',
validator: (value: string): boolean => ['mini', 'small', 'medium', 'large'].includes(value),
},
},
data() {
return {
filter: '',
};
},
computed: {
filteredUsers(): IUser[] {
return this.users.filter((user) => {
if (user.isPendingUser || !user.email) {
return false;
}
if (this.ignoreIds.includes(user.id)) { const props = withDefaults(defineProps<UserSelectProps>(), {
return false; users: () => [],
} modelValue: '',
ignoreIds: () => [],
if (user.fullName) { currentUserId: '',
const match = user.fullName.toLowerCase().includes(this.filter.toLowerCase());
if (match) {
return true;
}
}
return user.email.includes(this.filter);
});
},
sortedUsers(): IUser[] {
return [...this.filteredUsers].sort((a: IUser, b: IUser) => {
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName && b.firstName && a.firstName !== b.firstName) {
return a.firstName > b.firstName ? 1 : -1;
}
if (!a.email || !b.email) {
throw new Error('Expected all users to have email');
}
return a.email > b.email ? 1 : -1;
});
},
},
methods: {
setFilter(value: string) {
this.filter = value;
},
onBlur() {
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
},
getLabel(user: IUser) {
if (!user.fullName) {
return user.email;
}
return `${user.fullName} (${user.email})`;
},
},
}); });
const $emit = defineEmits(['blur', 'focus']);
const { t } = useI18n();
const filter = ref('');
const filteredUsers = computed(() =>
props.users.filter((user) => {
if (user.isPendingUser || !user.email) {
return false;
}
if (props.ignoreIds.includes(user.id)) {
return false;
}
if (user.fullName) {
const match = user.fullName.toLowerCase().includes(filter.value.toLowerCase());
if (match) {
return true;
}
}
return user.email.includes(filter.value);
}),
);
const sortedUsers = computed(() =>
[...filteredUsers.value].sort((a: IUser, b: IUser) => {
if (a.lastName && b.lastName && a.lastName !== b.lastName) {
return a.lastName > b.lastName ? 1 : -1;
}
if (a.firstName && b.firstName && a.firstName !== b.firstName) {
return a.firstName > b.firstName ? 1 : -1;
}
if (!a.email || !b.email) {
throw new Error('Expected all users to have email');
}
return a.email > b.email ? 1 : -1;
}),
);
const setFilter = (value: string) => {
filter.value = value;
};
const onBlur = () => $emit('blur');
const onFocus = () => $emit('focus');
const getLabel = (user: IUser) =>
!user.fullName ? user.email : `${user.fullName} (${user.email})`;
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -23,8 +23,8 @@ export default function () {
args = {} as unknown as Array<string | object>; args = {} as unknown as Array<string | object>;
} }
return str.replace(RE_NARGS, (match, prefix, i, index: number) => { return str.replace(RE_NARGS, (match, _, i, index: number) => {
let result; let result: string | object | null;
if (str[index - 1] === '{' && str[index + match.length] === '}') { if (str[index - 1] === '{' && str[index + match.length] === '}') {
return i; return i;

View file

@ -15,61 +15,52 @@
</table> </table>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import type { PropType } from 'vue'; import { onMounted, onUnmounted } from 'vue';
import { defineComponent } from 'vue';
export default defineComponent({ interface VariableTableProps {
name: 'VariableTable', variables: string[];
props: { attr?: string;
variables: { }
type: Array as PropType<string[]>,
required: true,
},
attr: {
type: String,
default: '',
},
},
data() {
return {
observer: null as null | MutationObserver,
values: {} as Record<string, string>,
};
},
created() {
const setValues = () => {
this.variables.forEach((variable) => {
const style = getComputedStyle(document.body);
const value = style.getPropertyValue(variable);
this.values = { const props = withDefaults(defineProps<VariableTableProps>(), {
...this.values, attr: '',
[variable]: value, });
};
});
};
setValues(); let observer: MutationObserver | null = null;
let values: Record<string, string> = {};
// when theme class is added or removed, reset color values onMounted(() => {
this.observer = new MutationObserver((mutationsList) => { const setValues = () => {
for (const mutation of mutationsList) { props.variables.forEach((variable) => {
if (mutation.type === 'attributes') { const style = getComputedStyle(document.body);
setValues(); const value = style.getPropertyValue(variable);
}
} values = {
...values,
[variable]: value,
};
}); });
const body = document.querySelector('body'); };
if (body) {
this.observer.observe(body, { attributes: true }); setValues();
// when theme class is added or removed, reset color values
observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
setValues();
}
} }
}, });
unmounted() { const body = document.querySelector('body');
if (this.observer) { if (body) {
this.observer.disconnect(); observer.observe(body, { attributes: true });
} }
}, });
onUnmounted(() => {
observer?.disconnect();
}); });
</script> </script>

View file

@ -9,42 +9,37 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { computed } from 'vue';
export default defineComponent({ interface SpacingPreviewProps {
name: 'SpacingPreview', property?: 'padding' | 'margin';
props: { side?: string;
property: { }
type: String,
default: 'padding', const SIZES = [
}, '0',
side: { '5xs',
type: String, '4xs',
default: '', '3xs',
}, '2xs',
}, 'xs',
computed: { 's',
sizes() { 'm',
return [ 'l',
'0', 'xl',
'5xs', '2xl',
'4xs', '3xl',
'3xs', '4xl',
'2xs', '5xl',
'xs', ] as const;
's',
'm', const props = withDefaults(defineProps<SpacingPreviewProps>(), {
'l', property: 'padding',
'xl', side: '',
'2xl',
'3xl',
'4xl',
'5xl',
].concat(this.property === 'margin' ? ['auto'] : []);
},
},
}); });
const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]);
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -4,7 +4,7 @@ export default {
title: 'Utilities/Lists', title: 'Utilities/Lists',
}; };
const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({ const ListStyleNoneTemplate: StoryFn = (_, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
template: `<ul class="list-style-none"> template: `<ul class="list-style-none">
<li>List item 1</li> <li>List item 1</li>
@ -15,7 +15,7 @@ const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({
export const StyleNone = ListStyleNoneTemplate.bind({}); export const StyleNone = ListStyleNoneTemplate.bind({});
const ListInlineTemplate: StoryFn = (args, { argTypes }) => ({ const ListInlineTemplate: StoryFn = (_, { argTypes }) => ({
props: Object.keys(argTypes), props: Object.keys(argTypes),
template: `<ul class="list-inline"> template: `<ul class="list-inline">
<li>List item 1</li> <li>List item 1</li>

View file

@ -3,6 +3,6 @@ export * from './datatable';
export * from './form'; export * from './form';
export * from './i18n'; export * from './i18n';
export * from './menu'; export * from './menu';
export * from './router'; export * from './select';
export * from './user'; export * from './user';
export * from './keyboardshortcut'; export * from './keyboardshortcut';

View file

@ -8,7 +8,7 @@ export type IMenuItem = {
icon?: string; icon?: string;
secondaryIcon?: { secondaryIcon?: {
name: string; name: string;
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'; size?: 'xsmall' | 'small' | 'medium' | 'large';
tooltip?: Partial<ElTooltipProps>; tooltip?: Partial<ElTooltipProps>;
}; };
customIconSize?: 'medium' | 'small'; customIconSize?: 'medium' | 'small';

View file

@ -1,4 +0,0 @@
export interface RouteObject {
name: string;
path: string;
}

View file

@ -0,0 +1,2 @@
const SELECT_SIZES = ['mini', 'small', 'medium', 'large', 'xlarge'] as const;
export type SelectSize = (typeof SELECT_SIZES)[number];

View file

@ -1,11 +1,5 @@
import { createEventBus } from '../event-bus'; import { createEventBus } from '../event-bus';
// @TODO: Remove when conflicting vitest and jest globals are reconciled
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const vi: (typeof import('vitest'))['vitest'];
}
describe('createEventBus()', () => { describe('createEventBus()', () => {
const eventBus = createEventBus(); const eventBus = createEventBus();

View file

@ -19,7 +19,6 @@
// TODO: remove all options below this line // TODO: remove all options below this line
"strict": false, "strict": false,
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedLocals": false,
"noImplicitReturns": false "noImplicitReturns": false
}, },
"include": ["src/**/*.ts", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.vue"]