mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(editor): Continue porting components over to composition API (no-changelog) (#8893)
This commit is contained in:
parent
80c4bc443a
commit
6c693e1afd
|
@ -19,6 +19,7 @@
|
|||
import { computed, useCssModule } from 'vue';
|
||||
import N8nText from '../N8nText';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import type { IconSize } from '@/types/icon';
|
||||
|
||||
const THEMES = ['info', 'success', 'secondary', 'warning', 'danger', 'custom'] as const;
|
||||
export type CalloutTheme = (typeof THEMES)[number];
|
||||
|
@ -33,7 +34,7 @@ const CALLOUT_DEFAULT_ICONS = {
|
|||
interface CalloutProps {
|
||||
theme: CalloutTheme;
|
||||
icon?: string;
|
||||
iconSize?: string;
|
||||
iconSize?: IconSize;
|
||||
iconless?: boolean;
|
||||
slim?: boolean;
|
||||
roundCorners?: boolean;
|
||||
|
@ -58,7 +59,7 @@ const getIcon = computed(
|
|||
() => props.icon ?? CALLOUT_DEFAULT_ICONS?.[props.theme] ?? CALLOUT_DEFAULT_ICONS.info,
|
||||
);
|
||||
|
||||
const getIconSize = computed(() => {
|
||||
const getIconSize = computed<IconSize>(() => {
|
||||
if (props.iconSize) {
|
||||
return props.iconSize;
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@
|
|||
<slot v-if="hasDefaultSlot" />
|
||||
<N8nSelect
|
||||
v-else-if="type === 'select' || type === 'multi-select'"
|
||||
ref="inputRef"
|
||||
:class="{ [$style.multiSelectSmallTags]: tagSize === 'small' }"
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:multiple="type === 'multi-select'"
|
||||
ref="inputRef"
|
||||
:disabled="disabled"
|
||||
:name="name"
|
||||
:teleported="teleported"
|
||||
|
@ -57,8 +57,8 @@
|
|||
</N8nSelect>
|
||||
<N8nInput
|
||||
v-else
|
||||
:name="name"
|
||||
ref="inputRef"
|
||||
:name="name"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:model-value="modelValue"
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import N8nFormInput from '../N8nFormInput';
|
||||
import type { IFormInput } from '../../types';
|
||||
import type { IFormInput, Validatable } from '../../types';
|
||||
import ResizeObserver from '../ResizeObserver';
|
||||
import type { EventBus } from '../../utils';
|
||||
import { createEventBus } from '../../utils';
|
||||
|
@ -83,7 +83,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
showValidationWarnings: false,
|
||||
values: {} as { [key: string]: unknown },
|
||||
values: {} as { [key: string]: Validatable },
|
||||
validity: {} as { [key: string]: boolean },
|
||||
};
|
||||
},
|
||||
|
@ -141,12 +141,15 @@ export default defineComponent({
|
|||
this.showValidationWarnings = true;
|
||||
|
||||
if (this.isReadyToSubmit) {
|
||||
const toSubmit = this.filteredInputs.reduce<{ [key: string]: unknown }>((accu, input) => {
|
||||
if (this.values[input.name]) {
|
||||
accu[input.name] = this.values[input.name];
|
||||
}
|
||||
return accu;
|
||||
}, {});
|
||||
const toSubmit = this.filteredInputs.reduce(
|
||||
(accu, input) => {
|
||||
if (this.values[input.name]) {
|
||||
accu[input.name] = this.values[input.name];
|
||||
}
|
||||
return accu;
|
||||
},
|
||||
{} as { [key: string]: Validatable },
|
||||
);
|
||||
this.$emit('submit', toSubmit);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import N8nText from '../N8nText';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import type { IconColor } from '@/types/icon';
|
||||
|
@ -70,16 +70,16 @@ const props = withDefaults(defineProps<InfoAccordionProps>(), {
|
|||
});
|
||||
const $emit = defineEmits(['click:body', 'tooltipClick']);
|
||||
|
||||
let expanded = false;
|
||||
const expanded = ref(false);
|
||||
onMounted(() => {
|
||||
props.eventBus.on('expand', () => {
|
||||
expanded = true;
|
||||
expanded.value = true;
|
||||
});
|
||||
expanded = props.initiallyExpanded;
|
||||
expanded.value = props.initiallyExpanded;
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
expanded = !expanded;
|
||||
expanded.value = !expanded.value;
|
||||
};
|
||||
|
||||
const onClick = (e: MouseEvent) => $emit('click:body', e);
|
||||
|
|
|
@ -9,15 +9,16 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import N8nText from '../N8nText';
|
||||
import N8nRoute, { type RouteTo } from '../N8nRoute';
|
||||
import N8nRoute from '../N8nRoute';
|
||||
import type { TextSize } from '@/types/text';
|
||||
|
||||
const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
|
||||
|
||||
interface LinkProps {
|
||||
to?: RouteLocationRaw;
|
||||
size?: TextSize;
|
||||
to?: RouteTo;
|
||||
newWindow?: boolean;
|
||||
bold?: boolean;
|
||||
underline?: boolean;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
v-html="htmlContent"
|
||||
/>
|
||||
<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" />
|
||||
<div :class="$style.spacer" />
|
||||
</div>
|
||||
|
|
|
@ -53,104 +53,76 @@
|
|||
</div>
|
||||
</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 N8nMenuItem from '../N8nMenuItem';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { IMenuItem, RouteObject } from '../../types';
|
||||
import type { IMenuItem } from '../../types';
|
||||
import { doesMenuItemMatchCurrentRoute } from '../N8nMenuItem/routerUtil';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nMenu',
|
||||
components: {
|
||||
ElMenu,
|
||||
N8nMenuItem,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: 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),
|
||||
);
|
||||
interface MenuProps {
|
||||
type?: 'primary' | 'secondary';
|
||||
defaultActive?: string;
|
||||
collapsed?: boolean;
|
||||
transparentBackground?: boolean;
|
||||
mode?: 'router' | 'tabs';
|
||||
tooltipDelay?: number;
|
||||
items?: IMenuItem[];
|
||||
modelValue?: string;
|
||||
}
|
||||
|
||||
this.activeTab = found ? found.id : '';
|
||||
} else {
|
||||
this.activeTab = this.items.length > 0 ? this.items[0].id : '';
|
||||
}
|
||||
|
||||
this.$emit('update:modelValue', this.activeTab);
|
||||
},
|
||||
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 props = withDefaults(defineProps<MenuProps>(), {
|
||||
type: 'primary',
|
||||
collapsed: false,
|
||||
transparentBackground: false,
|
||||
mode: 'router',
|
||||
tooltipDelay: 300,
|
||||
items: () => [],
|
||||
});
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
}"
|
||||
data-test-id="menu-item"
|
||||
:index="item.id"
|
||||
@click="handleSelect(item)"
|
||||
@click="handleSelect?.(item)"
|
||||
>
|
||||
<N8nIcon
|
||||
v-if="item.icon"
|
||||
|
@ -80,94 +80,67 @@
|
|||
</div>
|
||||
</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 N8nTooltip from '../N8nTooltip';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import ConditionalRouterLink from '../ConditionalRouterLink';
|
||||
import type { IMenuItem, RouteObject } from '../../types';
|
||||
import type { IMenuItem } from '../../types';
|
||||
import { doesMenuItemMatchCurrentRoute } from './routerUtil';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nMenuItem',
|
||||
components: {
|
||||
ElSubMenu,
|
||||
ElMenuItem,
|
||||
N8nIcon,
|
||||
N8nTooltip,
|
||||
ConditionalRouterLink,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object as PropType<IMenuItem>,
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
interface MenuItemProps {
|
||||
item: IMenuItem;
|
||||
compact?: boolean;
|
||||
tooltipDelay?: number;
|
||||
popperClass?: string;
|
||||
mode?: 'router' | 'tabs';
|
||||
activeTab?: string;
|
||||
handleSelect?: (item: IMenuItem) => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<MenuItemProps>(), {
|
||||
compact: false,
|
||||
tooltipDelay: 300,
|
||||
popperClass: '',
|
||||
mode: 'router',
|
||||
});
|
||||
|
||||
const $style = useCssModule();
|
||||
const $route = useRoute();
|
||||
|
||||
const availableChildren = computed((): IMenuItem[] =>
|
||||
Array.isArray(props.item.children)
|
||||
? props.item.children.filter((child) => child.available !== false)
|
||||
: [],
|
||||
);
|
||||
|
||||
const currentRoute = computed(() => {
|
||||
return $route ?? { name: '', path: '' };
|
||||
});
|
||||
|
||||
const submenuPopperClass = computed((): string => {
|
||||
const popperClass = [$style.submenuPopper, props.popperClass];
|
||||
if (props.compact) {
|
||||
popperClass.push($style.compact);
|
||||
}
|
||||
return popperClass.join(' ');
|
||||
});
|
||||
|
||||
const isActive = (item: IMenuItem): boolean => {
|
||||
if (props.mode === 'router') {
|
||||
return doesMenuItemMatchCurrentRoute(item, currentRoute.value);
|
||||
} else {
|
||||
return item.id === props.activeTab;
|
||||
}
|
||||
};
|
||||
|
||||
const isItemActive = (item: IMenuItem): boolean => {
|
||||
const hasActiveChild =
|
||||
Array.isArray(item.children) && item.children.some((child) => isActive(child));
|
||||
return isActive(item) || hasActiveChild;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import type { IMenuItem, RouteObject } from '@/types';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import type { IMenuItem } from '@/types';
|
||||
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 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[] = [];
|
||||
if (Array.isArray(item.activateOnRouteNames)) {
|
||||
activateOnRouteNames = item.activateOnRouteNames;
|
||||
|
@ -20,7 +23,7 @@ export function doesMenuItemMatchCurrentRoute(item: IMenuItem, currentRoute: Rou
|
|||
}
|
||||
|
||||
return (
|
||||
activateOnRouteNames.includes(currentRoute.name ?? '') ||
|
||||
activateOnRouteNames.includes((currentRoute.name as string) ?? '') ||
|
||||
activateOnRoutePaths.includes(currentRoute.path)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,99 +35,75 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
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({
|
||||
name: 'N8nNodeIcon',
|
||||
components: {
|
||||
N8nTooltip,
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value: string): boolean => ['file', 'icon', 'unknown'].includes(value),
|
||||
},
|
||||
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 || '',
|
||||
};
|
||||
}
|
||||
interface NodeIconProps {
|
||||
type: 'file' | 'icon' | 'unknown';
|
||||
src?: string;
|
||||
name?: string;
|
||||
nodeTypeName?: string;
|
||||
size?: number;
|
||||
disabled?: boolean;
|
||||
circle?: boolean;
|
||||
color?: string;
|
||||
showTooltip?: boolean;
|
||||
tooltipPosition?: Placement;
|
||||
badge?: { src: string; type: string };
|
||||
}
|
||||
|
||||
return {
|
||||
color: this.color || '',
|
||||
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 {};
|
||||
}
|
||||
const props = withDefaults(defineProps<NodeIconProps>(), {
|
||||
tooltipPosition: 'top',
|
||||
});
|
||||
|
||||
return {
|
||||
'max-width': `${this.size}px`,
|
||||
};
|
||||
},
|
||||
},
|
||||
const iconStyleData = computed((): Record<string, string> => {
|
||||
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>
|
||||
|
||||
|
|
|
@ -58,18 +58,17 @@ Sanitized.args = {
|
|||
'<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
|
||||
};
|
||||
|
||||
export const Truncated = PropTemplate.bind({});
|
||||
Truncated.args = {
|
||||
export const FullContent = PropTemplate.bind({});
|
||||
FullContent.args = {
|
||||
theme: 'warning',
|
||||
truncate: true,
|
||||
content:
|
||||
'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.',
|
||||
content: 'This is just the summary. <a data-key="toggle-expand">Show more</a>',
|
||||
fullContent:
|
||||
'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({});
|
||||
HtmlEdgeCase.args = {
|
||||
theme: 'warning',
|
||||
truncate: true,
|
||||
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.',
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:id="`${id}-content`"
|
||||
:class="showFullContent ? $style['expanded'] : $style['truncated']"
|
||||
role="region"
|
||||
v-html="sanitizeHtml(showFullContent ? fullContent : content)"
|
||||
v-html="displayContent"
|
||||
/>
|
||||
</slot>
|
||||
</N8nText>
|
||||
|
@ -15,89 +15,70 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, useCssModule } from 'vue';
|
||||
import sanitize from 'sanitize-html';
|
||||
import N8nText from '../../components/N8nText';
|
||||
import Locale from '../../mixins/locale';
|
||||
import { uid } from '../../utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nNotice',
|
||||
directives: {},
|
||||
components: {
|
||||
N8nText,
|
||||
},
|
||||
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;
|
||||
interface NoticeProps {
|
||||
id?: string;
|
||||
theme?: 'success' | 'warning' | 'danger' | 'info';
|
||||
content?: string;
|
||||
fullContent?: string;
|
||||
}
|
||||
|
||||
if (event.target.localName !== 'a') return;
|
||||
|
||||
if (event.target.dataset?.key) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
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 props = withDefaults(defineProps<NoticeProps>(), {
|
||||
id: () => uid('notice'),
|
||||
theme: 'warning',
|
||||
content: '',
|
||||
fullContent: '',
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -8,12 +8,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nPulse',
|
||||
});
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'N8nPulse' });
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
[$style.container]: true,
|
||||
[$style.hoverable]: !disabled,
|
||||
}"
|
||||
aria-checked="true"
|
||||
:aria-checked="active"
|
||||
>
|
||||
<div
|
||||
:class="{
|
||||
|
@ -23,33 +23,19 @@
|
|||
</label>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
interface RadioButtonProps {
|
||||
label: string;
|
||||
value: string;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nRadioButton',
|
||||
props: {
|
||||
label: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
withDefaults(defineProps<RadioButtonProps>(), {
|
||||
active: false,
|
||||
disabled: false,
|
||||
size: 'medium',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -15,48 +15,41 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import RadioButton from './RadioButton.vue';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export interface RadioOption {
|
||||
interface RadioOption {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nRadioButtons',
|
||||
components: {
|
||||
RadioButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<RadioOption[]>,
|
||||
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);
|
||||
},
|
||||
},
|
||||
interface RadioButtonsProps {
|
||||
modelValue?: string;
|
||||
options?: RadioOption[];
|
||||
size: 'small' | 'medium';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<RadioButtonsProps>(), {
|
||||
active: false,
|
||||
disabled: false,
|
||||
size: 'medium',
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { ComponentInstance } from 'vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
import N8nRecycleScroller from './RecycleScroller.vue';
|
||||
import type { ComponentInstance } from 'vue';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/RecycleScroller',
|
||||
|
@ -23,7 +23,9 @@ const Template: StoryFn = (args) => ({
|
|||
},
|
||||
methods: {
|
||||
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';
|
||||
itemRef.style.height = '200px';
|
||||
|
|
|
@ -1,225 +1,182 @@
|
|||
<script lang="ts">
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
<script lang="ts" setup>
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
import { computed, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
|
||||
|
||||
import type { PropType, ComponentPublicInstance } from 'vue';
|
||||
import { computed, defineComponent, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue';
|
||||
interface RecycleScrollerProps {
|
||||
itemSize: number;
|
||||
items: Array<Record<string, string>>;
|
||||
itemKey: string;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nRecycleScroller',
|
||||
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 props = withDefaults(defineProps<RecycleScrollerProps>(), {
|
||||
offset: 2,
|
||||
});
|
||||
|
||||
const scrollTop = ref(0);
|
||||
const wrapperHeight = ref(0);
|
||||
const windowHeight = ref(0);
|
||||
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 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 itemPositionCache = computed(() => {
|
||||
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
||||
const key = item[props.itemKey];
|
||||
const prevItem = props.items[index - 1];
|
||||
const prevItemPosition = prevItem ? acc[prevItem[props.itemKey]] : 0;
|
||||
const prevItemSize = prevItem ? itemSizeCache.value[prevItem[props.itemKey]] : 0;
|
||||
const itemSizeCache = ref<Record<string, number>>({});
|
||||
const itemPositionCache = computed(() => {
|
||||
return props.items.reduce<Record<string, number>>((acc, item, index) => {
|
||||
const key = item[props.itemKey];
|
||||
const prevItem = props.items[index - 1];
|
||||
const prevItemPosition = prevItem ? acc[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 foundIndex =
|
||||
props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
const startIndex = computed(() => {
|
||||
const foundIndex =
|
||||
props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
|
||||
return itemPosition >= scrollTop.value;
|
||||
}) - 1;
|
||||
const index = foundIndex - props.offset;
|
||||
return itemPosition >= scrollTop.value;
|
||||
}) - 1;
|
||||
const index = foundIndex - props.offset;
|
||||
|
||||
return index < 0 ? 0 : index;
|
||||
});
|
||||
return index < 0 ? 0 : index;
|
||||
});
|
||||
|
||||
const endIndex = computed(() => {
|
||||
const foundIndex = props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
const itemSize = itemSizeCache.value[item[props.itemKey]];
|
||||
const endIndex = computed(() => {
|
||||
const foundIndex = props.items.findIndex((item) => {
|
||||
const itemPosition = itemPositionCache.value[item[props.itemKey]];
|
||||
const itemSize = itemSizeCache.value[item[props.itemKey]];
|
||||
|
||||
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
|
||||
});
|
||||
const index = foundIndex + props.offset;
|
||||
return itemPosition + itemSize >= scrollTop.value + wrapperHeight.value;
|
||||
});
|
||||
const index = foundIndex + props.offset;
|
||||
|
||||
return foundIndex === -1 ? props.items.length - 1 : index;
|
||||
});
|
||||
return foundIndex === -1 ? props.items.length - 1 : index;
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
return props.items.slice(startIndex.value, endIndex.value + 1);
|
||||
});
|
||||
const visibleItems = computed(() => {
|
||||
return props.items.slice(startIndex.value, endIndex.value + 1);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => visibleItems.value,
|
||||
(currentValue, previousValue) => {
|
||||
const difference = currentValue.filter(
|
||||
(currentItem) =>
|
||||
!previousValue.find(
|
||||
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
|
||||
),
|
||||
);
|
||||
|
||||
if (difference.length > 0) {
|
||||
updateItemSizeCache(difference);
|
||||
}
|
||||
},
|
||||
watch(
|
||||
() => visibleItems.value,
|
||||
(currentValue, previousValue) => {
|
||||
const difference = currentValue.filter(
|
||||
(currentItem) =>
|
||||
!previousValue.find(
|
||||
(previousItem) => previousItem[props.itemKey] === currentItem[props.itemKey],
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Computed sizes and styles
|
||||
*/
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
if (difference.length > 0) {
|
||||
updateItemSizeCache(difference);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function updateItemSizeCache(items: Array<Record<string, string>>) {
|
||||
for (const item of items) {
|
||||
onUpdateItemSize(item);
|
||||
}
|
||||
}
|
||||
/** Computed sizes and styles */
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
itemSizeCache.value = {
|
||||
...itemSizeCache.value,
|
||||
[item[props.itemKey]]: size,
|
||||
};
|
||||
return lastItemPosition + lastItemSize;
|
||||
});
|
||||
|
||||
if (wrapperRef.value && scrollTop.value) {
|
||||
wrapperRef.value.scrollTop = wrapperRef.value.scrollTop + difference;
|
||||
scrollTop.value = wrapperRef.value.scrollTop;
|
||||
}
|
||||
});
|
||||
}
|
||||
const scrollerStyles = computed(() => ({
|
||||
height: `${scrollerHeight.value}px`,
|
||||
}));
|
||||
|
||||
function onWindowResize() {
|
||||
if (wrapperRef.value) {
|
||||
wrapperHeight.value = wrapperRef.value.offsetHeight;
|
||||
void nextTick(() => {
|
||||
updateItemSizeCache(visibleItems.value);
|
||||
});
|
||||
}
|
||||
const itemsStyles = computed(() => {
|
||||
const offset = itemPositionCache.value[props.items[startIndex.value][props.itemKey]];
|
||||
|
||||
windowHeight.value = window.innerHeight;
|
||||
}
|
||||
return {
|
||||
transform: `translateY(${offset}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
function onScroll() {
|
||||
if (!wrapperRef.value) {
|
||||
return;
|
||||
}
|
||||
/** 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>>) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
itemCount,
|
||||
itemSizeCache,
|
||||
itemPositionCache,
|
||||
itemsVisible: visibleItems,
|
||||
itemsStyles,
|
||||
scrollerStyles,
|
||||
scrollerScrollTop: scrollTop,
|
||||
scrollerRef,
|
||||
wrapperRef,
|
||||
itemsRef,
|
||||
itemRefs,
|
||||
onUpdateItemSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
function onWindowResize() {
|
||||
if (wrapperRef.value) {
|
||||
wrapperHeight.value = wrapperRef.value.offsetHeight;
|
||||
void nextTick(() => {
|
||||
updateItemSizeCache(visibleItems.value);
|
||||
});
|
||||
}
|
||||
|
||||
windowHeight.value = window.innerHeight;
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (!wrapperRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollTop.value = wrapperRef.value.scrollTop;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -227,7 +184,7 @@ export default defineComponent({
|
|||
<div ref="scrollerRef" class="recycle-scroller" :style="scrollerStyles">
|
||||
<div ref="itemsRef" class="recycle-scroller-items-wrapper" :style="itemsStyles">
|
||||
<div
|
||||
v-for="item in itemsVisible"
|
||||
v-for="item in visibleItems"
|
||||
:key="item[itemKey]"
|
||||
:ref="(element) => (itemRefs[item[itemKey]] = element)"
|
||||
class="recycle-scroller-item"
|
||||
|
|
|
@ -3,7 +3,7 @@ import N8nRecycleScroller from '../RecycleScroller.vue';
|
|||
|
||||
const itemSize = 100;
|
||||
const itemKey = 'id';
|
||||
const items = [...(new Array(100) as number[])].map((item, index) => ({
|
||||
const items = [...(new Array(100) as number[])].map((_, index) => ({
|
||||
id: index,
|
||||
name: `Item ${index}`,
|
||||
}));
|
||||
|
|
|
@ -11,9 +11,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
function closestNumber(value: number, divisor: number): number {
|
||||
const q = value / divisor;
|
||||
|
@ -35,7 +34,7 @@ function getSize(min: number, virtual: number, gridSize: number): number {
|
|||
return min;
|
||||
}
|
||||
|
||||
const directionsCursorMaps: { [key: string]: string } = {
|
||||
const directionsCursorMaps = {
|
||||
right: 'ew-resize',
|
||||
top: 'ns-resize',
|
||||
bottom: 'ns-resize',
|
||||
|
@ -44,141 +43,141 @@ const directionsCursorMaps: { [key: string]: string } = {
|
|||
topRight: 'ne-resize',
|
||||
bottomLeft: 'sw-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({
|
||||
name: 'N8nResize',
|
||||
props: {
|
||||
isResizingEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
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);
|
||||
const mouseMove = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let dWidth = 0;
|
||||
let dHeight = 0;
|
||||
let top = false;
|
||||
let left = false;
|
||||
|
||||
if (!this.isResizingEnabled) return [];
|
||||
if (this.supportedDirections.length === 0) return availableDirections;
|
||||
if (state.dir.value.includes('right')) {
|
||||
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;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resizerMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const deltaWidth = (dWidth - state.dWidth.value) / props.scale;
|
||||
const deltaHeight = (dHeight - state.dHeight.value) / props.scale;
|
||||
|
||||
const targetResizer = event.target as { dataset: { dir: string } } | null;
|
||||
if (targetResizer) {
|
||||
this.dir = targetResizer.dataset.dir.toLocaleLowerCase();
|
||||
}
|
||||
state.vHeight.value = state.vHeight.value + deltaHeight;
|
||||
state.vWidth.value = state.vWidth.value + deltaWidth;
|
||||
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;
|
||||
this.y = event.pageY;
|
||||
this.dWidth = 0;
|
||||
this.dHeight = 0;
|
||||
this.vHeight = this.height;
|
||||
this.vWidth = this.width;
|
||||
$emit('resize', { height, width, dX, dY, x, y, direction });
|
||||
state.dHeight.value = dHeight;
|
||||
state.dWidth.value = dWidth;
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', this.mouseMove);
|
||||
window.addEventListener('mouseup', this.mouseUp);
|
||||
this.$emit('resizestart');
|
||||
},
|
||||
mouseMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let dWidth = 0;
|
||||
let dHeight = 0;
|
||||
let top = false;
|
||||
let left = false;
|
||||
const mouseUp = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$emit('resizeend');
|
||||
window.removeEventListener('mousemove', mouseMove);
|
||||
window.removeEventListener('mouseup', mouseUp);
|
||||
document.body.style.cursor = 'unset';
|
||||
state.dir.value = '';
|
||||
};
|
||||
|
||||
if (this.dir.includes('right')) {
|
||||
dWidth = event.pageX - this.x;
|
||||
}
|
||||
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 resizerMove = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const deltaWidth = (dWidth - this.dWidth) / this.scale;
|
||||
const deltaHeight = (dHeight - this.dHeight) / this.scale;
|
||||
const targetResizer = event.target as { dataset: { dir: string } } | null;
|
||||
if (targetResizer) {
|
||||
state.dir.value = targetResizer.dataset.dir.toLocaleLowerCase() as Direction;
|
||||
}
|
||||
|
||||
this.vHeight = this.vHeight + deltaHeight;
|
||||
this.vWidth = this.vWidth + deltaWidth;
|
||||
const height = getSize(this.minHeight, this.vHeight, this.gridSize);
|
||||
const width = getSize(this.minWidth, this.vWidth, this.gridSize);
|
||||
document.body.style.cursor = directionsCursorMaps[state.dir.value];
|
||||
|
||||
const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
|
||||
const dY = top && height !== this.height ? -1 * (height - this.height) : 0;
|
||||
const x = event.x;
|
||||
const y = event.y;
|
||||
const direction = this.dir;
|
||||
state.x.value = event.pageX;
|
||||
state.y.value = event.pageY;
|
||||
state.dWidth.value = 0;
|
||||
state.dHeight.value = 0;
|
||||
state.vHeight.value = props.height;
|
||||
state.vWidth.value = props.width;
|
||||
|
||||
this.$emit('resize', { height, width, dX, dY, x, y, direction });
|
||||
this.dHeight = dHeight;
|
||||
this.dWidth = dWidth;
|
||||
},
|
||||
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 = '';
|
||||
},
|
||||
},
|
||||
});
|
||||
window.addEventListener('mousemove', mouseMove);
|
||||
window.addEventListener('mouseup', mouseUp);
|
||||
$emit('resizestart');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -9,12 +9,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// TODO: replace `object` with a more detailed type
|
||||
export type RouteTo = string | object;
|
||||
import { type RouteLocationRaw } from 'vue-router';
|
||||
|
||||
interface RouteProps {
|
||||
to?: RouteTo;
|
||||
to?: RouteLocationRaw;
|
||||
newWindow?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
import N8nRoute from './Route.vue';
|
||||
export type { RouteTo } from './Route.vue';
|
||||
export default N8nRoute;
|
||||
|
|
|
@ -30,16 +30,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
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>;
|
||||
|
||||
export interface IProps {
|
||||
size?: string;
|
||||
limitPopperWidth?: string;
|
||||
popperClass?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nSelect',
|
||||
components: {
|
||||
|
@ -49,10 +44,8 @@ export default defineComponent({
|
|||
...ElSelect.props,
|
||||
modelValue: {},
|
||||
size: {
|
||||
type: String,
|
||||
type: String as PropType<SelectSize>,
|
||||
default: 'large',
|
||||
validator: (value: string): boolean =>
|
||||
['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
|
@ -101,7 +94,7 @@ export default defineComponent({
|
|||
return acc;
|
||||
}, {});
|
||||
},
|
||||
computedSize(): string | undefined {
|
||||
computedSize(): InnerSelectRef['$props']['size'] {
|
||||
if (this.size === 'medium') {
|
||||
return 'default';
|
||||
}
|
||||
|
|
|
@ -57,150 +57,120 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import N8nInput from '../N8nInput';
|
||||
import N8nMarkdown from '../N8nMarkdown';
|
||||
import N8nResizeWrapper from '../N8nResizeWrapper';
|
||||
import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
|
||||
import N8nText from '../N8nText';
|
||||
import Locale from '../../mixins/locale';
|
||||
import { defineComponent } from 'vue';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nSticky',
|
||||
components: {
|
||||
N8nInput,
|
||||
N8nMarkdown,
|
||||
N8nResizeWrapper,
|
||||
N8nText,
|
||||
},
|
||||
mixins: [Locale],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: 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`,
|
||||
};
|
||||
interface StickyProps {
|
||||
modelValue?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
minHeight?: number;
|
||||
minWidth?: number;
|
||||
scale?: number;
|
||||
gridSize?: number;
|
||||
id?: string;
|
||||
defaultText?: string;
|
||||
editMode?: boolean;
|
||||
readOnly?: boolean;
|
||||
backgroundColor?: number | string;
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
shouldShowFooter(): boolean {
|
||||
return this.resHeight > 100 && this.resWidth > 155;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editMode(newMode, prevMode) {
|
||||
setTimeout(() => {
|
||||
if (newMode && !prevMode && this.$refs.input) {
|
||||
const textarea = this.$refs.input as HTMLTextAreaElement;
|
||||
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 props = withDefaults(defineProps<StickyProps>(), {
|
||||
height: 180,
|
||||
width: 240,
|
||||
minHeight: 80,
|
||||
minWidth: 150,
|
||||
scale: 1,
|
||||
gridSize: 20,
|
||||
id: '0',
|
||||
editMode: false,
|
||||
readOnly: false,
|
||||
backgroundColor: 1,
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -36,8 +36,12 @@ export const Example = Template.bind({});
|
|||
Example.args = {
|
||||
options: [
|
||||
{
|
||||
label: 'Test',
|
||||
value: 'test',
|
||||
label: 'First',
|
||||
value: 'first',
|
||||
},
|
||||
{
|
||||
label: 'Second',
|
||||
value: 'second',
|
||||
},
|
||||
{
|
||||
label: 'Github',
|
||||
|
|
|
@ -47,12 +47,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
export interface N8nTabOptions {
|
||||
interface TabOptions {
|
||||
value: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
|
@ -61,85 +60,63 @@ export interface N8nTabOptions {
|
|||
align?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nTabs',
|
||||
components: {
|
||||
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;
|
||||
interface TabsProps {
|
||||
modelValue?: string;
|
||||
options?: TabOptions[];
|
||||
}
|
||||
|
||||
this.canScrollRight = scrollWidth - width > this.scrollPosition;
|
||||
});
|
||||
|
||||
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' });
|
||||
}
|
||||
},
|
||||
},
|
||||
withDefaults(defineProps<TabsProps>(), {
|
||||
options: () => [],
|
||||
});
|
||||
|
||||
type ScrollByFunction = (arg: {
|
||||
left: number;
|
||||
top: number;
|
||||
behavior: 'smooth' | 'instant' | 'auto';
|
||||
}) => void;
|
||||
const scrollPosition = ref(0);
|
||||
const canScrollRight = ref(false);
|
||||
const tabs = ref<Element | undefined>(undefined);
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import N8nTag from '../N8nTag';
|
||||
import N8nLink from '../N8nLink';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
|
@ -46,11 +46,11 @@ const $emit = defineEmits(['expand', 'click:tag']);
|
|||
|
||||
const { t } = useI18n();
|
||||
|
||||
let showAll = false;
|
||||
const showAll = ref(false);
|
||||
|
||||
const visibleTags = computed((): ITag[] => {
|
||||
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;
|
||||
|
@ -59,7 +59,7 @@ const visibleTags = computed((): ITag[] => {
|
|||
const hiddenTagsLength = computed((): number => props.tags.length - props.truncateAt);
|
||||
|
||||
const onExpand = () => {
|
||||
showAll = true;
|
||||
showAll.value = true;
|
||||
$emit('expand', true);
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
:value="value[label]"
|
||||
: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>
|
||||
</template>
|
||||
</n8n-tree>
|
||||
|
@ -26,60 +26,51 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, useCssModule } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nTree',
|
||||
components: {},
|
||||
props: {
|
||||
value: {
|
||||
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;
|
||||
}
|
||||
interface TreeProps {
|
||||
value?: Record<string, unknown>;
|
||||
path?: string[];
|
||||
depth?: number;
|
||||
nodeClass?: string;
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && Object.keys(data).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return typeof data !== 'object';
|
||||
},
|
||||
getPath(key: string): unknown[] {
|
||||
if (Array.isArray(this.value)) {
|
||||
return [...this.path, parseInt(key, 10)];
|
||||
}
|
||||
return [...this.path, key];
|
||||
},
|
||||
},
|
||||
defineOptions({ name: 'N8nTree' });
|
||||
const props = withDefaults(defineProps<TreeProps>(), {
|
||||
value: () => ({}),
|
||||
path: () => [],
|
||||
depth: 0,
|
||||
nodeClass: '',
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
:model-value="modelValue"
|
||||
:filterable="true"
|
||||
:filter-method="setFilter"
|
||||
:placeholder="placeholder"
|
||||
:placeholder="placeholder || t('nds.userSelect.selectUser')"
|
||||
:default-first-option="true"
|
||||
teleported
|
||||
:popper-class="$style.limitPopperWidth"
|
||||
|
@ -30,114 +30,83 @@
|
|||
</N8nSelect>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import N8nUserInfo from '../N8nUserInfo';
|
||||
import N8nSelect from '../N8nSelect';
|
||||
import N8nOption from '../N8nOption';
|
||||
import type { IUser } from '../../types';
|
||||
import Locale from '../../mixins/locale';
|
||||
import { t } from '../../locale';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
import type { IUser, SelectSize } from '../../types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'N8nUserSelect',
|
||||
components: {
|
||||
N8nUserInfo,
|
||||
N8nSelect,
|
||||
N8nOption,
|
||||
},
|
||||
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;
|
||||
}
|
||||
interface UserSelectProps {
|
||||
users?: IUser[];
|
||||
modelValue?: string;
|
||||
ignoreIds?: string[];
|
||||
currentUserId?: string;
|
||||
placeholder?: string;
|
||||
size?: Exclude<SelectSize, 'xlarge'>;
|
||||
}
|
||||
|
||||
if (this.ignoreIds.includes(user.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.fullName) {
|
||||
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 props = withDefaults(defineProps<UserSelectProps>(), {
|
||||
users: () => [],
|
||||
modelValue: '',
|
||||
ignoreIds: () => [],
|
||||
currentUserId: '',
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -23,8 +23,8 @@ export default function () {
|
|||
args = {} as unknown as Array<string | object>;
|
||||
}
|
||||
|
||||
return str.replace(RE_NARGS, (match, prefix, i, index: number) => {
|
||||
let result;
|
||||
return str.replace(RE_NARGS, (match, _, i, index: number) => {
|
||||
let result: string | object | null;
|
||||
|
||||
if (str[index - 1] === '{' && str[index + match.length] === '}') {
|
||||
return i;
|
||||
|
|
|
@ -15,61 +15,52 @@
|
|||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VariableTable',
|
||||
props: {
|
||||
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);
|
||||
interface VariableTableProps {
|
||||
variables: string[];
|
||||
attr?: string;
|
||||
}
|
||||
|
||||
this.values = {
|
||||
...this.values,
|
||||
[variable]: value,
|
||||
};
|
||||
});
|
||||
};
|
||||
const props = withDefaults(defineProps<VariableTableProps>(), {
|
||||
attr: '',
|
||||
});
|
||||
|
||||
setValues();
|
||||
let observer: MutationObserver | null = null;
|
||||
let values: Record<string, string> = {};
|
||||
|
||||
// when theme class is added or removed, reset color values
|
||||
this.observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
setValues();
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
const setValues = () => {
|
||||
props.variables.forEach((variable) => {
|
||||
const style = getComputedStyle(document.body);
|
||||
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() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
||||
const body = document.querySelector('body');
|
||||
if (body) {
|
||||
observer.observe(body, { attributes: true });
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,42 +9,37 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SpacingPreview',
|
||||
props: {
|
||||
property: {
|
||||
type: String,
|
||||
default: 'padding',
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sizes() {
|
||||
return [
|
||||
'0',
|
||||
'5xs',
|
||||
'4xs',
|
||||
'3xs',
|
||||
'2xs',
|
||||
'xs',
|
||||
's',
|
||||
'm',
|
||||
'l',
|
||||
'xl',
|
||||
'2xl',
|
||||
'3xl',
|
||||
'4xl',
|
||||
'5xl',
|
||||
].concat(this.property === 'margin' ? ['auto'] : []);
|
||||
},
|
||||
},
|
||||
interface SpacingPreviewProps {
|
||||
property?: 'padding' | 'margin';
|
||||
side?: string;
|
||||
}
|
||||
|
||||
const SIZES = [
|
||||
'0',
|
||||
'5xs',
|
||||
'4xs',
|
||||
'3xs',
|
||||
'2xs',
|
||||
'xs',
|
||||
's',
|
||||
'm',
|
||||
'l',
|
||||
'xl',
|
||||
'2xl',
|
||||
'3xl',
|
||||
'4xl',
|
||||
'5xl',
|
||||
] as const;
|
||||
|
||||
const props = withDefaults(defineProps<SpacingPreviewProps>(), {
|
||||
property: 'padding',
|
||||
side: '',
|
||||
});
|
||||
|
||||
const sizes = computed(() => [...SIZES, ...(props.property === 'margin' ? ['auto'] : [])]);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -4,7 +4,7 @@ export default {
|
|||
title: 'Utilities/Lists',
|
||||
};
|
||||
|
||||
const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
const ListStyleNoneTemplate: StoryFn = (_, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
template: `<ul class="list-style-none">
|
||||
<li>List item 1</li>
|
||||
|
@ -15,7 +15,7 @@ const ListStyleNoneTemplate: StoryFn = (args, { argTypes }) => ({
|
|||
|
||||
export const StyleNone = ListStyleNoneTemplate.bind({});
|
||||
|
||||
const ListInlineTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
const ListInlineTemplate: StoryFn = (_, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
template: `<ul class="list-inline">
|
||||
<li>List item 1</li>
|
||||
|
|
|
@ -3,6 +3,6 @@ export * from './datatable';
|
|||
export * from './form';
|
||||
export * from './i18n';
|
||||
export * from './menu';
|
||||
export * from './router';
|
||||
export * from './select';
|
||||
export * from './user';
|
||||
export * from './keyboardshortcut';
|
||||
|
|
|
@ -8,7 +8,7 @@ export type IMenuItem = {
|
|||
icon?: string;
|
||||
secondaryIcon?: {
|
||||
name: string;
|
||||
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
|
||||
size?: 'xsmall' | 'small' | 'medium' | 'large';
|
||||
tooltip?: Partial<ElTooltipProps>;
|
||||
};
|
||||
customIconSize?: 'medium' | 'small';
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export interface RouteObject {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
2
packages/design-system/src/types/select.ts
Normal file
2
packages/design-system/src/types/select.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
const SELECT_SIZES = ['mini', 'small', 'medium', 'large', 'xlarge'] as const;
|
||||
export type SelectSize = (typeof SELECT_SIZES)[number];
|
|
@ -1,11 +1,5 @@
|
|||
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()', () => {
|
||||
const eventBus = createEventBus();
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
// TODO: remove all options below this line
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitReturns": false
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
|
|
Loading…
Reference in a new issue