Compare commits
1 Commits
feat_path_
...
lite/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be7801903 |
@@ -20,6 +20,7 @@
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@vueform/multiselect": "^2.6.2",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"complex-matcher": "^0.7.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "reset.css";
|
||||
@import "theme.css";
|
||||
@import "multi-select.css";
|
||||
/* TODO Serve fonts locally */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;0,900;1,400;1,500;1,600;1,700;1,900&display=swap");
|
||||
|
||||
|
||||
164
@xen-orchestra/lite/src/assets/multi-select.css
Normal file
164
@xen-orchestra/lite/src/assets/multi-select.css
Normal file
@@ -0,0 +1,164 @@
|
||||
@import "@vueform/multiselect/themes/default.css";
|
||||
|
||||
:root {
|
||||
--ms-font-size: 0.8em;
|
||||
--ms-line-height: 1.375;
|
||||
--ms-bg: var(--background-color-primary);
|
||||
--ms-bg-disabled: var(--background-color-secondary);
|
||||
--ms-border-color: var(--color-blue-scale-400);
|
||||
--ms-border-width: 0.1rem;
|
||||
--ms-border-color-active: var(--color-extra-blue-base);
|
||||
--ms-border-width-active: 0.1rem;
|
||||
--ms-radius: 0.4em;
|
||||
--ms-py: 1.08em;
|
||||
--ms-px: 0.625em;
|
||||
--ms-ring-width: 0;
|
||||
--ms-ring-color: transparent;
|
||||
--ms-placeholder-color: var(--color-blue-scale-100);
|
||||
--ms-max-height: 35rem;
|
||||
|
||||
--ms-spinner-color: var(--color-green-infra-base);
|
||||
--ms-caret-color: var(--color-blue-scale-300);
|
||||
--ms-clear-color: var(--color-blue-scale-300);
|
||||
--ms-clear-color-hover: var(--color-blue-scale-100);
|
||||
|
||||
--ms-tag-font-size: 1em;
|
||||
--ms-tag-line-height: 150%;
|
||||
--ms-tag-font-weight: 400;
|
||||
--ms-tag-bg: var(--background-color-secondary);
|
||||
--ms-tag-bg-disabled: var(--color-grayscale-200);
|
||||
--ms-tag-color: var(--color-blue-scale-200);
|
||||
--ms-tag-color-disabled: var(--color-blue-scale-500);
|
||||
--ms-tag-radius: 0.4em;
|
||||
--ms-tag-py: 0.4rem;
|
||||
--ms-tag-px: 1.2rem;
|
||||
--ms-tag-my: 0.25rem;
|
||||
--ms-tag-mx: 0.5rem;
|
||||
|
||||
--ms-tag-remove-radius: 4rem;
|
||||
--ms-tag-remove-py: 0.5rem;
|
||||
--ms-tag-remove-px: 0.5rem;
|
||||
--ms-tag-remove-my: 0rem;
|
||||
--ms-tag-remove-mx: 0.5rem;
|
||||
|
||||
--ms-dropdown-bg: var(--background-color-primary);
|
||||
--ms-dropdown-border-color: var(--color-extra-blue-base);
|
||||
--ms-dropdown-border-width: 0.1rem;
|
||||
--ms-dropdown-radius: 0.8rem;
|
||||
|
||||
--ms-group-label-py: 0.5rem;
|
||||
--ms-group-label-px: 2rem;
|
||||
--ms-group-label-line-height: 1.375;
|
||||
--ms-group-label-bg: var(--background-color-secondary);
|
||||
--ms-group-label-color: var(--color-blue-scale-100);
|
||||
--ms-group-label-bg-pointed: var(--color-blue-scale-400);
|
||||
--ms-group-label-color-pointed: var(--color-blue-scale-200);
|
||||
--ms-group-label-bg-disabled: var(--color-blue-scale-200);
|
||||
--ms-group-label-color-disabled: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected: var(--color-green-infra-base);
|
||||
--ms-group-label-color-selected: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected-pointed: var(--color-green-infra-d20);
|
||||
--ms-group-label-color-selected-pointed: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected-disabled: var(--color-blue-scale-200);
|
||||
--ms-group-label-color-selected-disabled: var(--color-green-infra-base);
|
||||
|
||||
--ms-option-font-size: 1em;
|
||||
--ms-option-line-height: 1.375;
|
||||
--ms-option-bg-pointed: var(--background-color-secondary);
|
||||
--ms-option-color-pointed: var(--color-blue-scale-200);
|
||||
--ms-option-bg-selected: var(--background-color-primary);
|
||||
--ms-option-color-selected: var(--color-green-infra-base);
|
||||
--ms-option-bg-disabled: var(--background-color-primary);
|
||||
--ms-option-color-disabled: var(--color-blue-scale-400);
|
||||
--ms-option-bg-selected-pointed: var(--background-color-secondary);
|
||||
--ms-option-color-selected-pointed: var(--color-green-infra-base);
|
||||
--ms-option-bg-selected-disabled: var(--background-color-primary);
|
||||
--ms-option-color-selected-disabled: var(--color-blue-scale-300);
|
||||
--ms-option-py: 1rem;
|
||||
--ms-option-px: 2rem;
|
||||
|
||||
--ms-empty-color: var(--color-grayscale-200);
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
min-width: 15rem;
|
||||
box-shadow: var(--shadow-100);
|
||||
|
||||
&:not(.is-disabled) {
|
||||
&.color-info {
|
||||
--ms-border-color: var(--color-blue-scale-400);
|
||||
--ms-border-color-active: var(--color-extra-blue-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-extra-blue-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-success {
|
||||
--ms-border-color: var(--color-green-infra-base);
|
||||
--ms-border-color-active: var(--color-green-infra-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-green-infra-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-warning {
|
||||
--ms-border-color: var(--color-orange-world-base);
|
||||
--ms-border-color-active: var(--color-orange-world-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-orange-world-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-error {
|
||||
--ms-border-color: var(--color-red-vates-base);
|
||||
--ms-border-color-active: var(--color-red-vates-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-red-vates-l60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .multiselect-group-label {
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
& .caret-icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0 var(--ms-px, 0.875rem) 0 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
transform: rotateX(0deg);
|
||||
z-index: 10;
|
||||
color: var(--ms-caret-color);
|
||||
}
|
||||
|
||||
&.is-open .caret-icon {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove {
|
||||
color: var(--background-color-secondary);
|
||||
background-color: var(--color-blue-scale-200);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove:hover {
|
||||
background-color: var(--color-red-vates-l40);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
& .multiselect-search,
|
||||
& .multiselect-tags-search {
|
||||
color: var(--color-blue-scale-100);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,15 @@ import {
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { uniqueId, upperFirst } from "lodash-es";
|
||||
import { computed, reactive, ref, watch, watchEffect } from "vue";
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
onBeforeUnmount,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const tab = (tab: TAB, params: Param[]) =>
|
||||
@@ -178,6 +186,24 @@ if (propParams.value.length !== 0) {
|
||||
}
|
||||
|
||||
const propValues = ref<Record<string, any>>({});
|
||||
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => {
|
||||
for (const param of props.params) {
|
||||
if (!isPropParam(param) || !param.hasChangeHandler()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => propValues.value[param.name],
|
||||
(value) => param.getOnChangeHandler()?.(value, propValues.value)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => scope.stop());
|
||||
|
||||
const settingValues = ref<Record<string, any>>({});
|
||||
const eventsLog = ref<
|
||||
{ id: string; name: string; args: { name: string; value: any }[] }[]
|
||||
@@ -236,8 +262,12 @@ const eventLogRows = computed(() => {
|
||||
const slotProperties = computed(() => {
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
propParams.value.forEach(({ name }) => {
|
||||
properties[name] = propValues.value[name];
|
||||
propParams.value.forEach((param) => {
|
||||
const value = propValues.value[param.name];
|
||||
|
||||
if (param.isRequired() || value !== undefined) {
|
||||
properties[param.name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
eventParams.value.forEach((eventParam) => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<th><!-- Reset Default --></th>
|
||||
<th><!-- Widget --></th>
|
||||
<th>Default</th>
|
||||
<th>Help</th>
|
||||
<th><!-- Help --></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
@@ -78,7 +78,11 @@
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="help">
|
||||
{{ param.getHelp() }}
|
||||
<UiIcon
|
||||
v-if="param.getHelp()"
|
||||
v-tooltip="param.getHelp()"
|
||||
:icon="faInfoCircle"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -95,7 +99,11 @@ import useModal from "@/composables/modal.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import type { PropParam } from "@/libs/story/story-param";
|
||||
import { faClose, faRepeat } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClose,
|
||||
faInfoCircle,
|
||||
faRepeat,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { toRef } from "vue";
|
||||
|
||||
@@ -168,6 +176,7 @@ const {
|
||||
.help {
|
||||
font-style: italic;
|
||||
color: var(--color-blue-scale-200);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.default-value {
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
<template>
|
||||
<FormSelect
|
||||
v-if="isSelectWidget(widget)"
|
||||
v-model="model"
|
||||
:wrapper-attrs="{ class: 'full-width' }"
|
||||
>
|
||||
<option v-if="!required && model === undefined" :value="undefined" />
|
||||
<option
|
||||
v-for="choice in widget.choices"
|
||||
:key="choice.label"
|
||||
:value="choice.value"
|
||||
>
|
||||
{{ choice.label }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<div v-else-if="isRadioWidget(widget)" class="radio">
|
||||
<FormInputWrapper v-for="choice in widget.choices" :key="choice.label">
|
||||
<FormRadio v-model="model" :value="choice.value" />
|
||||
{{ choice.label }}
|
||||
</FormInputWrapper>
|
||||
<div class="story-widget">
|
||||
<div v-if="isSelectWidget(widget)">
|
||||
<FormSelect :options="widget.choices" v-model="model" />
|
||||
</div>
|
||||
<div v-else-if="isRadioWidget(widget)" class="radio">
|
||||
<FormInputWrapper v-for="choice in widget.choices" :key="choice.label">
|
||||
<FormRadio v-model="model" :value="choice.value" />
|
||||
{{ choice.label }}
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div v-else-if="isBooleanWidget(widget)">
|
||||
<FormCheckbox v-model="model" />
|
||||
</div>
|
||||
<FormInput
|
||||
v-else-if="isNumberWidget(widget)"
|
||||
v-model.number="model"
|
||||
type="number"
|
||||
/>
|
||||
<FormInput v-else-if="isTextWidget(widget)" v-model="model" />
|
||||
<FormJson v-else-if="isObjectWidget(widget)" v-model="model" />
|
||||
</div>
|
||||
<div v-else-if="isBooleanWidget(widget)">
|
||||
<FormCheckbox v-model="model" />
|
||||
</div>
|
||||
<FormInput
|
||||
v-else-if="isNumberWidget(widget)"
|
||||
v-model.number="model"
|
||||
type="number"
|
||||
/>
|
||||
<FormInput v-else-if="isTextWidget(widget)" v-model="model" />
|
||||
<FormJson v-else-if="isObjectWidget(widget)" v-model="model" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -82,9 +73,11 @@ const model = useVModel(props, "modelValue", emit);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input,
|
||||
.form-json {
|
||||
font-size: 1.4rem;
|
||||
.story-widget {
|
||||
&:deep(.form-select),
|
||||
&:deep(.form-input),
|
||||
&:deep(.form-json) {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -157,7 +157,7 @@ defineExpose({
|
||||
max-width: 30em;
|
||||
|
||||
--before-width: v-bind('beforeWidth || "1.75em"');
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--after-width: v-bind('afterWidth || "1.75em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
@@ -187,9 +187,9 @@ defineExpose({
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
font-size: 1em;
|
||||
font-size: 0.8em;
|
||||
width: 100%;
|
||||
height: 3em;
|
||||
height: 3.5em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
border: 0.05em solid var(--border-color);
|
||||
@@ -292,11 +292,11 @@ defineExpose({
|
||||
padding-left: 0.625em;
|
||||
|
||||
&.has-before {
|
||||
padding-left: calc(var(--before-width) + 0.25em);
|
||||
padding-left: calc(var(--before-width) + 0.6em);
|
||||
}
|
||||
|
||||
&.has-after {
|
||||
padding-right: calc(var(--after-width) + 0.25em);
|
||||
padding-right: calc(var(--after-width) + 0.6em);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
:slotted(.form-input),
|
||||
:slotted(.form-select) {
|
||||
&:deep(.form-input),
|
||||
&:deep(.form-select) {
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
margin-left: -1px;
|
||||
|
||||
.input,
|
||||
.select {
|
||||
.select,
|
||||
.multiselect {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
@@ -31,7 +32,8 @@
|
||||
|
||||
&:not(:last-child) {
|
||||
.input,
|
||||
.select {
|
||||
.select,
|
||||
.multiselect {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { Color } from "@/types";
|
||||
import {
|
||||
IK_FORM_HAS_LABEL,
|
||||
IK_FORM_INPUT_COLOR,
|
||||
IK_FORM_LABEL_DISABLED,
|
||||
IK_INPUT_ID,
|
||||
} from "@/types/injection-keys";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
@@ -59,7 +58,6 @@ const props = defineProps<{
|
||||
warning?: string;
|
||||
error?: string;
|
||||
help?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const id = computed(() => props.id ?? uniqueId("form-input-"));
|
||||
@@ -83,11 +81,6 @@ provide(
|
||||
IK_FORM_HAS_LABEL,
|
||||
computed(() => slots.label !== undefined)
|
||||
);
|
||||
|
||||
provide(
|
||||
IK_FORM_LABEL_DISABLED,
|
||||
computed(() => props.disabled ?? false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,15 +1,93 @@
|
||||
<template>
|
||||
<FormInput>
|
||||
<slot />
|
||||
</FormInput>
|
||||
<span class="form-select">
|
||||
<MultiSelect
|
||||
v-model="modelValue"
|
||||
:can-clear="clearable"
|
||||
:class="colorClass"
|
||||
:close-on-deselect="!multiple"
|
||||
:close-on-select="!multiple"
|
||||
:groups="isGrouped"
|
||||
:hide-selected="false"
|
||||
:label="labelKey"
|
||||
:mode="multiple ? 'multiple' : 'single'"
|
||||
:multiple-label="getMultipleLabel"
|
||||
:no-options-text="$t('no-options-available')"
|
||||
:no-results-text="$t('no-results-found')"
|
||||
:options="options"
|
||||
:track-by="labelKey"
|
||||
:value-prop="valueKey"
|
||||
:object="object"
|
||||
:disabled="busy || disabled"
|
||||
:searchable="options.length > SEARCHABLE_THRESHOLD"
|
||||
:loading="busy"
|
||||
>
|
||||
<template #caret>
|
||||
<UiIcon :icon="faAngleDown" class="caret-icon" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import { IK_INPUT_TYPE } from "@/types/injection-keys";
|
||||
import { provide } from "vue";
|
||||
<script generic="T extends XenApiRecord<string>" lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { XenApiRecord } from "@/libs/xen-api";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_FORM_INPUT_COLOR } from "@/types/injection-keys";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import MultiSelect from "@vueform/multiselect";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { computed, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
provide(IK_INPUT_TYPE, "select");
|
||||
const SEARCHABLE_THRESHOLD = 10;
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: any;
|
||||
multiple?: boolean;
|
||||
options: { label: string; options: T[] }[] | T[];
|
||||
labelKey?: string;
|
||||
valueKey?: string;
|
||||
clearable?: boolean;
|
||||
color?: Color;
|
||||
object?: boolean;
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
}>(),
|
||||
{
|
||||
labelKey: "label",
|
||||
valueKey: "value",
|
||||
color: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const isGrouped = computed(() => {
|
||||
const option = props.options[0];
|
||||
return "object" === typeof option && "options" in option && "label" in option;
|
||||
});
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const getMultipleLabel = (values: any[]) =>
|
||||
i18n.t("n-options-selected", { n: values.length });
|
||||
|
||||
const parentColor = inject(IK_FORM_INPUT_COLOR, undefined);
|
||||
|
||||
const colorClass = computed(() => {
|
||||
const color = props.color ?? parentColor?.value ?? "info";
|
||||
|
||||
return `color-${color}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
<style lang="postcss" scoped>
|
||||
.form-select {
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
82
@xen-orchestra/lite/src/components/form/FormTag.vue
Normal file
82
@xen-orchestra/lite/src/components/form/FormTag.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<span class="form-tag">
|
||||
<MultiSelect
|
||||
v-model="modelValue"
|
||||
:class="colorClass"
|
||||
:create-option="allowNew"
|
||||
:no-options-text="$t('no-options-available')"
|
||||
:no-results-text="$t('no-results-found')"
|
||||
:on-create="handleCreate"
|
||||
:options="options"
|
||||
:searchable="allowNew || options.length > SEARCHABLE_THRESHOLD"
|
||||
mode="tags"
|
||||
@deselect="($event) => handleDeselect($event as string)"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #caret>
|
||||
<UiIcon :icon="faAngleDown" class="caret-icon" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_FORM_INPUT_COLOR } from "@/types/injection-keys";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import MultiSelect from "@vueform/multiselect";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { computed, inject } from "vue";
|
||||
|
||||
const SEARCHABLE_THRESHOLD = 10;
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: any;
|
||||
createdTags?: string[];
|
||||
options: string[];
|
||||
allowNew?: boolean;
|
||||
color?: Color;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{ createdTags: () => [] }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: string[]): void;
|
||||
(event: "update:createdTags", value: string[]): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
|
||||
const handleCreate = (tag: { label: string; value: string }) => {
|
||||
emit("update:createdTags", [...props.createdTags, tag.value]);
|
||||
return tag;
|
||||
};
|
||||
|
||||
const handleDeselect = (value: string) => {
|
||||
if (!props.allowNew) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"update:createdTags",
|
||||
props.createdTags.filter((t) => t !== value)
|
||||
);
|
||||
};
|
||||
|
||||
const parentColor = inject(IK_FORM_INPUT_COLOR, undefined);
|
||||
|
||||
const colorClass = computed(() => {
|
||||
const color = props.color ?? parentColor?.value ?? "info";
|
||||
|
||||
return `color-${color}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.form-tag {
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
31
@xen-orchestra/lite/src/components/form/FormXapiRecord.vue
Normal file
31
@xen-orchestra/lite/src/components/form/FormXapiRecord.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<FormSelect
|
||||
object
|
||||
v-model="modelValue"
|
||||
:color="color"
|
||||
:multiple="multiple"
|
||||
:options="options"
|
||||
label-key="name_label"
|
||||
value-key="$ref"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script generic="T extends XenApiRecord<string>" lang="ts" setup>
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import type { XenApiRecord } from "@/libs/xen-api";
|
||||
import type { Color } from "@/types";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
multiple?: boolean;
|
||||
options: { label: string; options: T[] }[] | T[];
|
||||
color?: Color;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
</script>
|
||||
@@ -102,12 +102,26 @@ export class PropParam extends mixin(BaseParam, WithWidget, WithType) {
|
||||
#isRequired = false;
|
||||
#defaultValue: any;
|
||||
#isVModel: boolean;
|
||||
#onChangeHandler: ((value: any, context: object) => void) | undefined;
|
||||
|
||||
constructor(name: string, isVModel = false) {
|
||||
super(name);
|
||||
this.#isVModel = isVModel;
|
||||
}
|
||||
|
||||
onChange(handler: (value: any, context: object) => void) {
|
||||
this.#onChangeHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
hasChangeHandler() {
|
||||
return this.#onChangeHandler !== undefined;
|
||||
}
|
||||
|
||||
getOnChangeHandler() {
|
||||
return this.#onChangeHandler;
|
||||
}
|
||||
|
||||
isRequired() {
|
||||
return this.#isRequired;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"log-out": "Log out",
|
||||
"login": "Login",
|
||||
"migrate": "Migrate",
|
||||
"n-options-selected": "{n} option selected | {n} options selected",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Name",
|
||||
"network": "Network",
|
||||
@@ -77,6 +78,8 @@
|
||||
"news": "News",
|
||||
"news-name": "{name} news",
|
||||
"new-features-are-coming": "New features are coming soon!",
|
||||
"no-options-available": "No options available",
|
||||
"no-results-found": "No results found",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
"or": "Or",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"log-out": "Se déconnecter",
|
||||
"login": "Connexion",
|
||||
"migrate": "Migrer",
|
||||
"n-options-selected": "{n} option sélectionnée | {n} options sélectionnées",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Nom",
|
||||
"network": "Réseau",
|
||||
@@ -77,6 +78,8 @@
|
||||
"news": "Actualités",
|
||||
"news-name": "Actualités {name}",
|
||||
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
|
||||
"no-options-available": "Aucune option disponible",
|
||||
"no-results-found": "Aucun résultat trouvé",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
"or": "Ou",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
```vue-template
|
||||
<FormInputGroup>
|
||||
<FormInput />
|
||||
<FormInput />
|
||||
<FormSelect>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</FormSelect>
|
||||
<FormInput ... />
|
||||
<FormInput ... />
|
||||
<FormSelect ... />
|
||||
</FormInputGroup>
|
||||
```
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
:params="[slot().help('Can contains multiple FormInput and FormSelect')]"
|
||||
>
|
||||
<FormInputGroup>
|
||||
<FormInput />
|
||||
<FormInput />
|
||||
<FormSelect>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</FormSelect>
|
||||
<FormInput v-model="model" />
|
||||
<FormInput v-model="model" />
|
||||
<FormSelect
|
||||
v-model="model"
|
||||
:options="['Option 1', 'Option 2', 'Option 3']"
|
||||
/>
|
||||
</FormInputGroup>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
@@ -20,4 +19,7 @@ import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputGroup from "@/components/form/FormInputGroup.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { slot } from "@/libs/story/story-param";
|
||||
import { ref } from "vue";
|
||||
|
||||
const model = ref("");
|
||||
</script>
|
||||
|
||||
72
@xen-orchestra/lite/src/stories/form-select.story.md
Normal file
72
@xen-orchestra/lite/src/stories/form-select.story.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `options` prop
|
||||
|
||||
## Array of strings
|
||||
|
||||
```ts
|
||||
const options = ["Option 1", "Option 2", "Option 3"];
|
||||
```
|
||||
|
||||
## Array of objects
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
### Custom properties
|
||||
|
||||
When not using `label` and `value` properties, you can change them with `label-key` and `value-key` props.
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ name: "Option 1", id: "option1" },
|
||||
{ name: "Option 2", id: "option2" },
|
||||
{ name: "Option 3", id: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
```html
|
||||
<FormSelect :options="options" label-key="name" value-key="id" />
|
||||
```
|
||||
|
||||
## Array of groups
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{
|
||||
label: "Group 1",
|
||||
options: [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Group 2",
|
||||
options: [
|
||||
{ label: "Option 4", value: "option4" },
|
||||
{ label: "Option 5", value: "option5" },
|
||||
{ label: "Option 6", value: "option6" },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
# `object` prop
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
By default, when selection "Option 2", the value sent to `v-model` will be `option2`.
|
||||
|
||||
If you want to send the whole object, you can use `object` prop.
|
||||
|
||||
In this case, the value sent to `v-model` will be `{ label: 'Option 2', value: 'option2' }`.
|
||||
60
@xen-orchestra/lite/src/stories/form-select.story.vue
Normal file
60
@xen-orchestra/lite/src/stories/form-select.story.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().required().type('any'),
|
||||
prop('options').required().arr().preset(options),
|
||||
prop('multiple').bool().widget().onChange(handleMultipleChange),
|
||||
prop('labelKey')
|
||||
.default('label')
|
||||
.str()
|
||||
.help(
|
||||
'If `options` is an array of objects, item label will be extracted from this key'
|
||||
),
|
||||
prop('valueKey')
|
||||
.default('value')
|
||||
.str()
|
||||
.help(
|
||||
'If `options` is an array of objects, item value will be extracted from this key'
|
||||
),
|
||||
prop('clearable')
|
||||
.bool()
|
||||
.widget()
|
||||
.help('When true, adds a clear button on the right side of the select'),
|
||||
colorProp(),
|
||||
prop('disabled').bool().widget(),
|
||||
prop('object')
|
||||
.bool()
|
||||
.widget()
|
||||
.help(
|
||||
'If `options` is an array of objects, the whole object will be selected instead of only the value'
|
||||
),
|
||||
]"
|
||||
>
|
||||
<FormSelect v-if="isActive" v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const options = [
|
||||
{ label: "Option 1", value: "1" },
|
||||
{ label: "Option 2", value: "2" },
|
||||
{ label: "Option 3", value: "3" },
|
||||
];
|
||||
|
||||
// Workaround to prevent errors when `multiple` changes
|
||||
const isActive = ref(true);
|
||||
|
||||
const handleMultipleChange = (isMultiple, context) => {
|
||||
isActive.value = false;
|
||||
context.modelValue = isMultiple ? [] : null;
|
||||
nextTick(() => {
|
||||
isActive.value = true;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
31
@xen-orchestra/lite/src/stories/form-tag.story.vue
Normal file
31
@xen-orchestra/lite/src/stories/form-tag.story.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model()
|
||||
.required()
|
||||
.type('string[]')
|
||||
.help('List of selected tags (including created ones)'),
|
||||
model('createdTags').type('string[]').help('List of created tags'),
|
||||
prop('options')
|
||||
.required()
|
||||
.arr('string')
|
||||
.preset(availableTags)
|
||||
.help('List of available tags')
|
||||
.widget(object()),
|
||||
prop('allowNew').bool().help('Allow to create new tags').widget(),
|
||||
colorProp(),
|
||||
]"
|
||||
>
|
||||
<FormTag v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormTag from "@/components/form/FormTag.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { object } from "@/libs/story/story-widget";
|
||||
|
||||
const availableTags = ["First tag", "Second tag", "Third tag"];
|
||||
</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
```typescript
|
||||
type XenApiRecordGroup = {
|
||||
label: string;
|
||||
options: XenApiRecord[];
|
||||
}[];
|
||||
```
|
||||
58
@xen-orchestra/lite/src/stories/form-xapi-record.story.vue
Normal file
58
@xen-orchestra/lite/src/stories/form-xapi-record.story.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('XenApiRecord').required(),
|
||||
prop('multiple').bool().widget().onChange(handleMultipleChange),
|
||||
prop('options')
|
||||
.required()
|
||||
.arr()
|
||||
.type('XenApiRecord[] | XenApiRecordGroup[]')
|
||||
.widget()
|
||||
.preset(options),
|
||||
colorProp(),
|
||||
prop('disabled').bool().widget(),
|
||||
]"
|
||||
>
|
||||
<FormXapiRecord v-if="isActive" v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormXapiRecord from "@/components/form/FormXapiRecord.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "ISOs - Storage Lab",
|
||||
options: [
|
||||
{
|
||||
$ref: "1",
|
||||
name_label: "AlmaLinux-8.3-x86_64-minimal.iso",
|
||||
},
|
||||
{
|
||||
$ref: "2",
|
||||
name_label: "AlmaLinux-8.5-x86_64-boot.iso",
|
||||
},
|
||||
{ $ref: "3", name_label: "CentOS-6.10-i386-minimal.iso" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "XCP-ng Tools - XO Lab",
|
||||
options: [{ $ref: "4", name_label: "guest-tools.iso" }],
|
||||
},
|
||||
];
|
||||
|
||||
// Workaround to prevent errors when `multiple` is changed
|
||||
const isActive = ref(true);
|
||||
|
||||
const handleMultipleChange = (isMultiple, context) => {
|
||||
isActive.value = false;
|
||||
context.modelValue = isMultiple ? [] : null;
|
||||
nextTick(() => {
|
||||
isActive.value = true;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
<div class="row">
|
||||
Choose a component
|
||||
<FormSelect v-model="componentPath">
|
||||
<option value="" />
|
||||
<option v-for="path in componentPaths" :key="path">
|
||||
{{ path }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<FormSelect v-model="componentPath" :options="componentPaths" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -3969,6 +3969,11 @@
|
||||
"@volar/typescript" "1.7.8"
|
||||
"@vue/language-core" "1.8.1"
|
||||
|
||||
"@vueform/multiselect@^2.6.2":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@vueform/multiselect/-/multiselect-2.6.2.tgz#2a14c5f7d9cf4ea07853b7f9b491c039bbfadf40"
|
||||
integrity sha512-4BFvXyzyi0Pqi/lsYdGwONsQy+ypiVzuQbmoDUlttTxuoujFtf9AdeHi1ZhfIorXG9BiysK1HcQSfYB6USgwfQ==
|
||||
|
||||
"@vuepress/core@1.9.9":
|
||||
version "1.9.9"
|
||||
resolved "https://registry.yarnpkg.com/@vuepress/core/-/core-1.9.9.tgz#aa8bc4497fcbb6aab9c1e290944d422edeb20495"
|
||||
|
||||
Reference in New Issue
Block a user