feat(lite): rework modal system (#6994)
This commit is contained in:
parent
79d48f3b56
commit
b11f11f4db
@ -14,66 +14,66 @@
|
||||
</UiActionButton>
|
||||
</UiFilterGroup>
|
||||
|
||||
<UiModal
|
||||
v-if="isOpen"
|
||||
:icon="faFilter"
|
||||
@submit.prevent="handleSubmit"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<div class="rows">
|
||||
<CollectionFilterRow
|
||||
v-for="(newFilter, index) in newFilters"
|
||||
:key="newFilter.id"
|
||||
v-model="newFilters[index]"
|
||||
:available-filters="availableFilters"
|
||||
@remove="removeNewFilter"
|
||||
/>
|
||||
</div>
|
||||
<UiModal v-model="isOpen">
|
||||
<ConfirmModalLayout @submit.prevent="handleSubmit">
|
||||
<template #default>
|
||||
<div class="rows">
|
||||
<CollectionFilterRow
|
||||
v-for="(newFilter, index) in newFilters"
|
||||
:key="newFilter.id"
|
||||
v-model="newFilters[index]"
|
||||
:available-filters="availableFilters"
|
||||
@remove="removeNewFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="newFilters.some((filter) => filter.isAdvanced)"
|
||||
class="available-properties"
|
||||
>
|
||||
{{ $t("available-properties-for-advanced-filter") }}
|
||||
<div class="properties">
|
||||
<UiBadge
|
||||
v-for="(filter, property) in availableFilters"
|
||||
:key="property"
|
||||
:icon="getFilterIcon(filter)"
|
||||
<div
|
||||
v-if="newFilters.some((filter) => filter.isAdvanced)"
|
||||
class="available-properties"
|
||||
>
|
||||
{{ property }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
{{ $t("available-properties-for-advanced-filter") }}
|
||||
<div class="properties">
|
||||
<UiBadge
|
||||
v-for="(filter, property) in availableFilters"
|
||||
:key="property"
|
||||
:icon="getFilterIcon(filter)"
|
||||
>
|
||||
{{ property }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="addNewFilter">
|
||||
{{ $t("add-or") }}
|
||||
</UiButton>
|
||||
<UiButton :disabled="!isFilterValid" type="submit">
|
||||
{{ $t(editedFilter ? "update" : "add") }}
|
||||
</UiButton>
|
||||
<UiButton outlined @click="handleCancel">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="addNewFilter">
|
||||
{{ $t("add-or") }}
|
||||
</UiButton>
|
||||
<UiButton :disabled="!isFilterValid" type="submit">
|
||||
{{ $t(editedFilter ? "update" : "add") }}
|
||||
</UiButton>
|
||||
<UiButton outlined @click="close">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Or, parse } from "complex-matcher";
|
||||
import { computed, ref } from "vue";
|
||||
import type { Filters, NewFilter } from "@/types/filter";
|
||||
import { faFilter, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import UiActionButton from "@/components/ui/UiActionButton.vue";
|
||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { getFilterIcon } from "@/libs/utils";
|
||||
import type { Filters, NewFilter } from "@/types/filter";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Or, parse } from "complex-matcher";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
defineProps<{
|
||||
activeFilters: string[];
|
||||
@ -85,7 +85,7 @@ const emit = defineEmits<{
|
||||
(event: "removeFilter", filter: string): void;
|
||||
}>();
|
||||
|
||||
const { isOpen, open, close } = useModal();
|
||||
const { isOpen, open, close } = useModal({ onClose: () => reset() });
|
||||
const newFilters = ref<NewFilter[]>([]);
|
||||
let newFilterId = 0;
|
||||
|
||||
@ -156,11 +156,6 @@ const handleSubmit = () => {
|
||||
reset();
|
||||
close();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
reset();
|
||||
close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@ -190,4 +185,10 @@ const handleCancel = () => {
|
||||
margin-top: 0.6rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -219,7 +219,6 @@ const valueInputAfter = computed(() =>
|
||||
.collection-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--background-color-secondary);
|
||||
gap: 1rem;
|
||||
|
||||
@ -242,4 +241,8 @@ const valueInputAfter = computed(() =>
|
||||
.form-widget-advanced {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ui-action-button:first-of-type {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,56 +17,56 @@
|
||||
</UiActionButton>
|
||||
</UiFilterGroup>
|
||||
|
||||
<UiModal
|
||||
v-if="isOpen"
|
||||
:icon="faSort"
|
||||
@submit.prevent="handleSubmit"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<div class="form-widgets">
|
||||
<FormWidget :label="$t('sort-by')">
|
||||
<select v-model="newSortProperty">
|
||||
<option v-if="!newSortProperty"></option>
|
||||
<option
|
||||
v-for="(sort, property) in availableSorts"
|
||||
:key="property"
|
||||
:value="property"
|
||||
>
|
||||
{{ sort.label ?? property }}
|
||||
</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
<FormWidget>
|
||||
<select v-model="newSortIsAscending">
|
||||
<option :value="true">{{ $t("ascending") }}</option>
|
||||
<option :value="false">{{ $t("descending") }}</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
</div>
|
||||
<template #buttons>
|
||||
<UiButton type="submit">{{ $t("add") }}</UiButton>
|
||||
<UiButton outlined @click="handleCancel">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
<UiModal v-model="isOpen">
|
||||
<ConfirmModalLayout @submit.prevent="handleSubmit">
|
||||
<template #default>
|
||||
<div class="form-widgets">
|
||||
<FormWidget :label="$t('sort-by')">
|
||||
<select v-model="newSortProperty">
|
||||
<option v-if="!newSortProperty"></option>
|
||||
<option
|
||||
v-for="(sort, property) in availableSorts"
|
||||
:key="property"
|
||||
:value="property"
|
||||
>
|
||||
{{ sort.label ?? property }}
|
||||
</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
<FormWidget>
|
||||
<select v-model="newSortIsAscending">
|
||||
<option :value="true">{{ $t("ascending") }}</option>
|
||||
<option :value="false">{{ $t("descending") }}</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton type="submit">{{ $t("add") }}</UiButton>
|
||||
<UiButton outlined @click="close">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import UiActionButton from "@/components/ui/UiActionButton.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import type { ActiveSorts, Sorts } from "@/types/sort";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCaretUp,
|
||||
faPlus,
|
||||
faSort,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ref } from "vue";
|
||||
|
||||
@ -81,7 +81,7 @@ const emit = defineEmits<{
|
||||
(event: "removeSort", property: string): void;
|
||||
}>();
|
||||
|
||||
const { isOpen, open, close } = useModal();
|
||||
const { isOpen, open, close } = useModal({ onClose: () => reset() });
|
||||
|
||||
const newSortProperty = ref();
|
||||
const newSortIsAscending = ref<boolean>(true);
|
||||
@ -96,11 +96,6 @@ const handleSubmit = () => {
|
||||
reset();
|
||||
close();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
reset();
|
||||
close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
@ -1,45 +1,58 @@
|
||||
<template>
|
||||
<UiModal
|
||||
v-if="isSslModalOpen"
|
||||
:icon="faServer"
|
||||
color="error"
|
||||
@close="clearUnreachableHostsUrls"
|
||||
>
|
||||
<template #title>{{ $t("unreachable-hosts") }}</template>
|
||||
<div class="description">
|
||||
<p>{{ $t("following-hosts-unreachable") }}</p>
|
||||
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
||||
<ul>
|
||||
<li v-for="url in unreachableHostsUrls" :key="url">
|
||||
<a :href="url" class="link" rel="noopener" target="_blank">{{
|
||||
url
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template #buttons>
|
||||
<UiButton color="success" @click="reload">
|
||||
{{ $t("unreachable-hosts-reload-page") }}
|
||||
</UiButton>
|
||||
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
|
||||
</template>
|
||||
<UiModal v-model="isSslModalOpen" color="error">
|
||||
<ConfirmModalLayout :icon="faServer">
|
||||
<template #title>{{ $t("unreachable-hosts") }}</template>
|
||||
|
||||
<template #default>
|
||||
<div class="description">
|
||||
<p>{{ $t("following-hosts-unreachable") }}</p>
|
||||
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
||||
<ul>
|
||||
<li v-for="url in unreachableHostsUrls" :key="url">
|
||||
<a :href="url" class="link" rel="noopener" target="_blank">{{
|
||||
url
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton color="success" @click="reload">
|
||||
{{ $t("unreachable-hosts-reload-page") }}
|
||||
</UiButton>
|
||||
<UiButton @click="closeSslModal">{{ $t("cancel") }}</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { useHostCollection } from "@/stores/xen-api/host.store";
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { difference } from "lodash-es";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const { records: hosts } = useHostCollection();
|
||||
const unreachableHostsUrls = ref<Set<string>>(new Set());
|
||||
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
|
||||
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
|
||||
const reload = () => window.location.reload();
|
||||
|
||||
const { isOpen: isSslModalOpen, close: closeSslModal } = useModal({
|
||||
onClose: () => unreachableHostsUrls.value.clear(),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unreachableHostsUrls.value.size,
|
||||
(size) => {
|
||||
isSslModalOpen.value = size > 0;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(hosts, (nextHosts, previousHosts) => {
|
||||
difference(nextHosts, previousHosts).forEach((host) => {
|
||||
const url = new URL("http://localhost");
|
||||
@ -53,7 +66,11 @@ watch(hosts, (nextHosts, previousHosts) => {
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.description p {
|
||||
margin: 1rem 0;
|
||||
.description {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -58,7 +58,7 @@ const getDefaultOpenedDirectories = (): Set<string> => {
|
||||
}
|
||||
|
||||
const openedDirectories = new Set<string>();
|
||||
const parts = currentRoute.path.split("/");
|
||||
const parts = currentRoute.path.split("/").slice(2);
|
||||
let currentPath = "";
|
||||
|
||||
for (const part of parts) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<UiModal v-if="isRawValueModalOpen" @close="closeRawValueModal">
|
||||
<CodeHighlight :code="rawValueModalPayload" />
|
||||
<UiModal v-model="isRawValueModalOpen">
|
||||
<BasicModalLayout>
|
||||
<CodeHighlight :code="rawValueModalPayload" />
|
||||
</BasicModalLayout>
|
||||
</UiModal>
|
||||
<StoryParamsTable>
|
||||
<thead>
|
||||
@ -99,7 +101,8 @@ import CodeHighlight from "@/components/CodeHighlight.vue";
|
||||
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
|
||||
import StoryWidget from "@/components/component-story/StoryWidget.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
@ -130,7 +133,6 @@ const model = useVModel(props, "modelValue", emit);
|
||||
|
||||
const {
|
||||
open: openRawValueModal,
|
||||
close: closeRawValueModal,
|
||||
isOpen: isRawValueModalOpen,
|
||||
payload: rawValueModalPayload,
|
||||
} = useModal<string>();
|
||||
|
@ -4,7 +4,7 @@
|
||||
v-if="label !== undefined || learnMoreUrl !== undefined"
|
||||
class="label-container"
|
||||
>
|
||||
<label :for="id" class="label">
|
||||
<label :class="{ light }" :for="id" class="label">
|
||||
<UiIcon :icon="icon" />
|
||||
{{ label }}
|
||||
</label>
|
||||
@ -58,6 +58,7 @@ const props = withDefaults(
|
||||
error?: string;
|
||||
help?: string;
|
||||
disabled?: boolean;
|
||||
light?: boolean;
|
||||
}>(),
|
||||
{ disabled: undefined }
|
||||
);
|
||||
@ -95,14 +96,24 @@ useContext(DisabledContext, () => props.disabled);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--color-blue-scale-100);
|
||||
font-size: 1.4rem;
|
||||
padding: 1rem 0;
|
||||
|
||||
&.light {
|
||||
font-size: 1.6rem;
|
||||
color: var(--color-blue-scale-300);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:not(.light) {
|
||||
font-size: 1.4rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--color-blue-scale-100);
|
||||
}
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
|
@ -1,20 +1,28 @@
|
||||
<template>
|
||||
<UiModal
|
||||
@submit.prevent="saveJson"
|
||||
v-model="isCodeModalOpen"
|
||||
:color="isJsonValid ? 'success' : 'error'"
|
||||
v-if="isCodeModalOpen"
|
||||
:icon="faCode"
|
||||
@close="closeCodeModal"
|
||||
closable
|
||||
>
|
||||
<FormTextarea class="modal-textarea" v-model="editedJson" />
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
|
||||
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
|
||||
<UiButton :disabled="!isJsonValid" type="submit"
|
||||
>{{ $t("save") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
<FormModalLayout @submit.prevent="saveJson" :icon="faCode">
|
||||
<template #default>
|
||||
<FormTextarea class="modal-textarea" v-model="editedJson" />
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="formatJson">
|
||||
{{ $t("reformat") }}
|
||||
</UiButton>
|
||||
<UiButton outlined @click="closeCodeModal">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
<UiButton :disabled="!isJsonValid" type="submit">
|
||||
{{ $t("save") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</FormModalLayout>
|
||||
</UiModal>
|
||||
|
||||
<FormInput
|
||||
@click="openCodeModal"
|
||||
:model-value="jsonValue"
|
||||
@ -26,8 +34,9 @@
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormTextarea from "@/components/form/FormTextarea.vue";
|
||||
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { faCode } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel, whenever } from "@vueuse/core";
|
||||
|
@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<form
|
||||
:class="className"
|
||||
class="ui-modal"
|
||||
v-bind="$attrs"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<div class="container">
|
||||
<span v-if="onClose" class="close-icon" @click="emit('close')">
|
||||
<UiIcon :icon="faXmark" />
|
||||
</span>
|
||||
<div v-if="icon || $slots.icon" class="modal-icon">
|
||||
<slot name="icon">
|
||||
<UiIcon :icon="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
<UiTitle v-if="$slots.title" type="h4">
|
||||
<slot name="title" />
|
||||
</UiTitle>
|
||||
<div v-if="$slots.subtitle" class="subtitle">
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<div v-if="$slots.default" class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<UiButtonGroup :color="color">
|
||||
<slot name="buttons" />
|
||||
</UiButtonGroup>
|
||||
</div>
|
||||
</form>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
icon?: IconDefinition;
|
||||
color?: "info" | "warning" | "error" | "success";
|
||||
onClose?: () => void;
|
||||
}>(),
|
||||
{ color: "info" }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { escape } = useMagicKeys();
|
||||
whenever(escape, () => emit("close"));
|
||||
|
||||
const className = computed(() => {
|
||||
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-modal {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #00000080;
|
||||
}
|
||||
|
||||
.color-success {
|
||||
--modal-color: var(--color-green-infra-base);
|
||||
--modal-background-color: var(--background-color-green-infra);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
--modal-color: var(--color-extra-blue-base);
|
||||
--modal-background-color: var(--background-color-extra-blue);
|
||||
}
|
||||
|
||||
.color-warning {
|
||||
--modal-color: var(--color-orange-world-base);
|
||||
--modal-background-color: var(--background-color-orange-world);
|
||||
}
|
||||
|
||||
.color-error {
|
||||
--modal-color: var(--color-red-vates-base);
|
||||
--modal-background-color: var(--background-color-red-vates);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 40rem;
|
||||
padding: 4.2rem;
|
||||
text-align: center;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--modal-background-color);
|
||||
box-shadow: var(--shadow-400);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 2rem;
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 2rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--modal-color);
|
||||
}
|
||||
|
||||
.container :deep(.accent) {
|
||||
color: var(--modal-color);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
font-size: 4.8rem;
|
||||
margin: 2rem 0;
|
||||
color: var(--modal-color);
|
||||
}
|
||||
|
||||
.ui-title {
|
||||
margin-top: 4rem;
|
||||
|
||||
.has-icon & {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-blue-scale-200);
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
font-size: 1.6rem;
|
||||
max-height: calc(100vh - 40rem);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.ui-button-group {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<UiIcon
|
||||
:class="textClass"
|
||||
:icon="faXmark"
|
||||
class="modal-close-icon"
|
||||
@click="close"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext } from "@/context";
|
||||
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { inject } from "vue";
|
||||
|
||||
const { textClass } = useContext(ColorContext);
|
||||
|
||||
const close = inject(IK_MODAL_CLOSE, undefined);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.modal-close-icon {
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="[backgroundClass, { nested: isNested }]"
|
||||
class="modal-container"
|
||||
>
|
||||
<header v-if="$slots.header" class="modal-header">
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<main v-if="$slots.default" class="modal-content">
|
||||
<slot name="default" />
|
||||
</main>
|
||||
<footer v-if="$slots.footer" class="modal-footer">
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext } from "@/context";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_MODAL_NESTED } from "@/types/injection-keys";
|
||||
import { inject, provide } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tag?: string;
|
||||
color?: Color;
|
||||
}>(),
|
||||
{ tag: "div" }
|
||||
);
|
||||
|
||||
defineSlots<{
|
||||
header: () => any;
|
||||
default: () => any;
|
||||
footer: () => any;
|
||||
}>();
|
||||
|
||||
const { backgroundClass } = useContext(ColorContext, () => props.color);
|
||||
|
||||
const isNested = inject(IK_MODAL_NESTED, false);
|
||||
provide(IK_MODAL_NESTED, true);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto 1fr;
|
||||
max-width: calc(100vw - 2rem);
|
||||
max-height: calc(100vh - 20rem);
|
||||
padding: 2rem;
|
||||
gap: 1rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 1.6rem;
|
||||
|
||||
&:not(.nested) {
|
||||
min-width: 40rem;
|
||||
box-shadow: var(--shadow-400);
|
||||
}
|
||||
|
||||
&.nested {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
text-align: center;
|
||||
grid-row: 2;
|
||||
padding: 2rem;
|
||||
max-height: 75vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
grid-row: 3;
|
||||
align-self: end;
|
||||
}
|
||||
</style>
|
57
@xen-orchestra/lite/src/components/ui/modals/UiModal.vue
Normal file
57
@xen-orchestra/lite/src/components/ui/modals/UiModal.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="ui-modal" @click.self="close">
|
||||
<slot />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext } from "@/context";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
|
||||
import { useMagicKeys, useVModel, whenever } from "@vueuse/core/index";
|
||||
import { provide } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
color?: Color;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const isOpen = useVModel(props, "modelValue", emit);
|
||||
|
||||
const close = () => (isOpen.value = false);
|
||||
|
||||
provide(IK_MODAL_CLOSE, close);
|
||||
|
||||
useContext(ColorContext, () => props.color);
|
||||
|
||||
const { escape } = useMagicKeys();
|
||||
|
||||
whenever(escape, () => close());
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-modal {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(26, 27, 56, 0.25);
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<ModalContainer>
|
||||
<template #header>
|
||||
<ModalCloseIcon class="close-icon" />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot />
|
||||
</template>
|
||||
</ModalContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
|
||||
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
|
||||
|
||||
defineSlots<{
|
||||
default: () => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.close-icon {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<ModalContainer tag="form">
|
||||
<template #header>
|
||||
<div class="close-bar">
|
||||
<ModalCloseIcon />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<UiIcon :class="textClass" :icon="icon" class="main-icon" />
|
||||
<div v-if="$slots.title || $slots.subtitle" class="titles">
|
||||
<UiTitle v-if="$slots.title" type="h4">
|
||||
<slot name="title" />
|
||||
</UiTitle>
|
||||
<div v-if="$slots.subtitle" class="subtitle">
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.default">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UiButtonGroup>
|
||||
<slot name="buttons" />
|
||||
</UiButtonGroup>
|
||||
</template>
|
||||
</ModalContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
|
||||
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext } from "@/context";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
icon?: IconDefinition;
|
||||
}>();
|
||||
|
||||
const { textClass } = useContext(ColorContext);
|
||||
|
||||
defineSlots<{
|
||||
title: () => void;
|
||||
subtitle: () => void;
|
||||
default: () => void;
|
||||
buttons: () => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.close-bar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
font-size: 4.8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-blue-scale-200);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<ModalContainer tag="form">
|
||||
<template #header>
|
||||
<div :class="borderClass" class="title-bar">
|
||||
<UiIcon :class="textClass" :icon="icon" />
|
||||
<slot name="title" />
|
||||
<ModalCloseIcon class="close-icon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UiButtonGroup class="footer-buttons">
|
||||
<slot name="buttons" />
|
||||
</UiButtonGroup>
|
||||
</template>
|
||||
</ModalContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
|
||||
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext, DisabledContext } from "@/context";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
icon?: IconDefinition;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{ disabled: undefined }
|
||||
);
|
||||
|
||||
defineSlots<{
|
||||
title: () => void;
|
||||
default: () => void;
|
||||
buttons: () => void;
|
||||
}>();
|
||||
|
||||
const { textClass, borderClass } = useContext(ColorContext);
|
||||
|
||||
useContext(DisabledContext, () => props.disabled);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.title-bar {
|
||||
display: flex;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
font-size: 2.4rem;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
margin-left: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
@ -1,42 +1,43 @@
|
||||
<template>
|
||||
<MenuItem
|
||||
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
|
||||
:disabled="areSomeVmsInExecution"
|
||||
:disabled="isDisabled"
|
||||
:icon="faTrashCan"
|
||||
@click="openDeleteModal"
|
||||
>
|
||||
{{ $t("delete") }}
|
||||
</MenuItem>
|
||||
<UiModal
|
||||
v-if="isDeleteModalOpen"
|
||||
:icon="faSatellite"
|
||||
@close="closeDeleteModal"
|
||||
>
|
||||
<template #title>
|
||||
<i18n-t keypath="confirm-delete" scope="global" tag="div">
|
||||
<span :class="textClass">
|
||||
{{ $t("n-vms", { n: vmRefs.length }) }}
|
||||
</span>
|
||||
</i18n-t>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
{{ $t("please-confirm") }}
|
||||
</template>
|
||||
<template #buttons>
|
||||
<UiButton outlined @click="closeDeleteModal">
|
||||
{{ $t("go-back") }}
|
||||
</UiButton>
|
||||
<UiButton @click="deleteVms">
|
||||
{{ $t("delete-vms", { n: vmRefs.length }) }}
|
||||
</UiButton>
|
||||
</template>
|
||||
<UiModal v-model="isDeleteModalOpen">
|
||||
<ConfirmModalLayout :icon="faSatellite">
|
||||
<template #title>
|
||||
<i18n-t keypath="confirm-delete" scope="global" tag="div">
|
||||
<span :class="textClass">
|
||||
{{ $t("n-vms", { n: vmRefs.length }) }}
|
||||
</span>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
{{ $t("please-confirm") }}
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton outlined @click="closeDeleteModal">
|
||||
{{ $t("go-back") }}
|
||||
</UiButton>
|
||||
<UiButton @click="deleteVms">
|
||||
{{ $t("delete-vms", { n: vmRefs.length }) }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import { useContext } from "@/composables/context.composable";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { ColorContext } from "@/context";
|
||||
@ -68,6 +69,10 @@ const areSomeVmsInExecution = computed(() =>
|
||||
vms.value.some((vm) => vm.power_state !== VM_POWER_STATE.HALTED)
|
||||
);
|
||||
|
||||
const isDisabled = computed(
|
||||
() => vms.value.length === 0 || areSomeVmsInExecution.value
|
||||
);
|
||||
|
||||
const deleteVms = async () => {
|
||||
await xenApi.vm.delete(props.vmRefs);
|
||||
closeDeleteModal();
|
||||
|
@ -1,16 +1,57 @@
|
||||
# useModal composable
|
||||
|
||||
### Usage
|
||||
|
||||
#### API
|
||||
|
||||
`useModal<T>(options: ModalOptions)`
|
||||
|
||||
Type parameter:
|
||||
|
||||
- `T`: The type for the modal's payload.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `options`: An optional object of type `ModalOptions`.
|
||||
|
||||
Returns an object with:
|
||||
|
||||
- `payload: ReadOnly<Ref<T | undefined>>`: The payload data of the modal. Mainly used if a single modal is used for
|
||||
multiple items (typically with `v-for`)
|
||||
- `isOpen: WritableComputedRef<boolean>`: A writable computed indicating if the modal is open or not.
|
||||
- `open(currentPayload?: T)`: A function to open the modal and optionally set its payload.
|
||||
- `close(force = false)`: A function to close the modal. If force is set to `true`, the modal will be closed without
|
||||
calling the `confirmClose` callback.
|
||||
|
||||
#### Types
|
||||
|
||||
`ModalOptions`
|
||||
|
||||
An object type that accepts:
|
||||
|
||||
- `confirmClose?: () => boolean`: An optional callback that is called before the modal is closed. If this function
|
||||
returns `false`, the modal will not be closed.
|
||||
- `onClose?: () => void`: An optional callback that is called after the modal is closed.
|
||||
|
||||
### Example
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div v-for="item in items">
|
||||
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
|
||||
{{ item.name }}
|
||||
<button @click="openRemoveModal(item)">Delete</button>
|
||||
</div>
|
||||
|
||||
<UiModal v-if="isRemoveModalOpen">
|
||||
Are you sure you want to delete {{ removeModalPayload.name }}
|
||||
|
||||
<button @click="handleRemove">Yes</button>
|
||||
<button @click="closeRemoveModal">No</button>
|
||||
<UiModal v-model="isRemoveModalOpen">
|
||||
<ModalContainer>
|
||||
<template #header>
|
||||
Are you sure you want to delete {{ removeModalPayload.name }}?
|
||||
</template>
|
||||
<template #footer>
|
||||
<button @click="handleRemove">Yes</button>
|
||||
<button @click="closeRemoveModal">No</button>
|
||||
</template>
|
||||
</ModalContainer>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
@ -22,7 +63,11 @@ const {
|
||||
isOpen: isRemoveModalOpen,
|
||||
open: openRemoveModal,
|
||||
close: closeRemoveModal,
|
||||
} = useModal();
|
||||
} = useModal({
|
||||
confirmClose: () =>
|
||||
window.confirm("Are you sure you want to close this modal?"),
|
||||
onClose: () => console.log("Modal closed"),
|
||||
});
|
||||
|
||||
async function handleRemove() {
|
||||
await removeItem(removeModalPayload.id);
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { ref } from "vue";
|
||||
import { computed, readonly, ref } from "vue";
|
||||
|
||||
export default function useModal<T>() {
|
||||
type ModalOptions = {
|
||||
confirmClose?: () => boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export default function useModal<T>(options: ModalOptions = {}) {
|
||||
const $payload = ref<T>();
|
||||
const $isOpen = ref(false);
|
||||
|
||||
@ -8,15 +13,35 @@ export default function useModal<T>() {
|
||||
$isOpen.value = true;
|
||||
$payload.value = payload;
|
||||
};
|
||||
const close = (force = false) => {
|
||||
if (!force && options.confirmClose?.() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.onClose) {
|
||||
options.onClose();
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
$isOpen.value = false;
|
||||
$payload.value = undefined;
|
||||
};
|
||||
|
||||
const isOpen = computed({
|
||||
get() {
|
||||
return $isOpen.value;
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
payload: $payload,
|
||||
isOpen: $isOpen,
|
||||
payload: readonly($payload),
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
};
|
||||
|
@ -9,6 +9,7 @@
|
||||
prop('error').type('string').widget(),
|
||||
prop('help').type('string').widget().preset('256 by default'),
|
||||
prop('disabled').type('boolean').widget().ctx(),
|
||||
prop('light').bool().widget(),
|
||||
slot().help('Contains the input'),
|
||||
]"
|
||||
>
|
||||
|
@ -0,0 +1,11 @@
|
||||
```vue-template
|
||||
<UiModal v-model="isOpen">
|
||||
<BasicModalLayout>
|
||||
Here is a basic modal...
|
||||
</BasicModalLayout>
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
const { isOpen } = useModal();
|
||||
```
|
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ settings }"
|
||||
:params="[
|
||||
slot(),
|
||||
setting('defaultSlotContent').preset('Modal content').widget(text()),
|
||||
]"
|
||||
>
|
||||
<BasicModalLayout>
|
||||
{{ settings.defaultSlotContent }}
|
||||
</BasicModalLayout>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
|
||||
import { setting, slot } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
@ -0,0 +1,21 @@
|
||||
```vue-template
|
||||
<UiModal v-model="isOpen">
|
||||
<ConfirmModalLayout :icon="faShip">
|
||||
<template #title>Do you confirm?</template>
|
||||
<template #subtitle>You should be sure about this</template>
|
||||
<template #buttons>
|
||||
<UiButton outlined @click="close">I prefer not</UiButton>
|
||||
<UiButton @click="accept">Yes, I'm sure!</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
const { isOpen, close } = useModal();
|
||||
|
||||
const accept = async () => {
|
||||
// do something
|
||||
close();
|
||||
}
|
||||
```
|
@ -1,45 +1,32 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
colorProp(),
|
||||
iconProp(),
|
||||
event('close').preset(close),
|
||||
slot('default'),
|
||||
slot('title'),
|
||||
slot('subtitle'),
|
||||
slot('icon'),
|
||||
slot('default'),
|
||||
slot('buttons').help('Meant to receive UiButton components'),
|
||||
setting('title').preset('Modal Title').widget(),
|
||||
setting('subtitle').preset('Modal Subtitle').widget(),
|
||||
]"
|
||||
v-slot="{ properties, settings }"
|
||||
>
|
||||
<UiButton type="button" @click="open">Open Modal</UiButton>
|
||||
|
||||
<UiModal v-bind="properties" v-if="isOpen">
|
||||
<ConfirmModalLayout v-bind="properties">
|
||||
<template #title>{{ settings.title }}</template>
|
||||
<template #subtitle>{{ settings.subtitle }}</template>
|
||||
<template #buttons>
|
||||
<UiButton @click="close">Discard</UiButton>
|
||||
<UiButton outlined>Discard</UiButton>
|
||||
<UiButton>Go</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
</ConfirmModalLayout>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import {
|
||||
colorProp,
|
||||
event,
|
||||
iconProp,
|
||||
setting,
|
||||
slot,
|
||||
} from "@/libs/story/story-param";
|
||||
|
||||
const { open, close, isOpen } = useModal();
|
||||
import { iconProp, setting, slot } from "@/libs/story/story-param";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
@ -0,0 +1,25 @@
|
||||
```vue-template
|
||||
<UiModal v-model="isOpen">
|
||||
<FormModalLayout :icon="faShip" @submit.prevent="handleSubmit">
|
||||
<template #title>Migrate 3 VMs/template>
|
||||
|
||||
<template #default>
|
||||
<!-- Form content goes here... -->
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton outlined @click="close">Cancel</UiButton>
|
||||
<UiButton type="submit">Migrate 3 VMs</UiButton>
|
||||
</template>
|
||||
</ConfirmModalLayout>
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
const { isOpen, close } = useModal();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Handling form submission...
|
||||
close();
|
||||
}
|
||||
```
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[iconProp(), slot('title'), slot('default'), slot('buttons')]"
|
||||
>
|
||||
<FormModalLayout :icon="faRoute" v-bind="properties">
|
||||
<template #title>Migrate 3 VMs</template>
|
||||
|
||||
<div>
|
||||
<FormInputWrapper
|
||||
label="Select a destination host"
|
||||
learn-more-url="http://..."
|
||||
light
|
||||
>
|
||||
<FormInput />
|
||||
</FormInputWrapper>
|
||||
|
||||
<FormInputWrapper
|
||||
label="Select a migration network (optional)"
|
||||
learn-more-url="http://..."
|
||||
light
|
||||
>
|
||||
<FormInput />
|
||||
</FormInputWrapper>
|
||||
|
||||
<FormInputWrapper
|
||||
help="Individual selection for each VDI is not available on multiple VMs migration."
|
||||
label="Select a destination SR"
|
||||
learn-more-url="http://..."
|
||||
light
|
||||
>
|
||||
<FormInput />
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
|
||||
<template #buttons>
|
||||
<UiButton outlined>Cancel</UiButton>
|
||||
<UiButton>Migrate 3 VMs</UiButton>
|
||||
</template>
|
||||
</FormModalLayout>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
|
||||
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import { iconProp, slot } from "@/libs/story/story-param";
|
||||
import { faRoute } from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
@ -0,0 +1,19 @@
|
||||
A basic modal container containing 3 slots: `header`, `default` and `footer`.
|
||||
|
||||
Tag will be `div` by default but can be changed with the `tag` prop.
|
||||
|
||||
Color can be changed with the `color` prop.
|
||||
|
||||
To keep the content centered vertically, header and footer will always have the same height.
|
||||
|
||||
Modal content has an max height + overflow to prevent the modal growing out of the screen.
|
||||
|
||||
Modal containers can be nested.
|
||||
|
||||
```vue-template
|
||||
<ModalContainer>
|
||||
<template #header>Header</template>
|
||||
<template #default>Content</template>
|
||||
<template #header>Footer</template>
|
||||
</ModalContainer>
|
||||
```
|
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('tag').str().default('div').widget(),
|
||||
colorProp(),
|
||||
slot('header'),
|
||||
slot(),
|
||||
slot('footer'),
|
||||
setting('headerSlotContent')
|
||||
.preset('Header')
|
||||
.widget(text())
|
||||
.help('Content for default slot'),
|
||||
setting('defaultSlotContent')
|
||||
.preset('Content')
|
||||
.widget(text())
|
||||
.help('Content for default slot'),
|
||||
setting('footerSlotContent')
|
||||
.preset('Footer')
|
||||
.widget(text())
|
||||
.help('Content for default slot'),
|
||||
setting('showNested')
|
||||
.preset(false)
|
||||
.widget(boolean())
|
||||
.help('Show nested modal'),
|
||||
]"
|
||||
>
|
||||
<ModalContainer v-bind="properties">
|
||||
<template #header>
|
||||
{{ settings.headerSlotContent }}
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
{{ settings.defaultSlotContent }}
|
||||
<ModalContainer v-if="settings.showNested" color="error">
|
||||
Nested modal
|
||||
</ModalContainer>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
{{ settings.footerSlotContent }}
|
||||
</template>
|
||||
</ModalContainer>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
|
||||
import { colorProp, prop, setting, slot } from "@/libs/story/story-param";
|
||||
import { boolean, text } from "@/libs/story/story-widget";
|
||||
</script>
|
21
@xen-orchestra/lite/src/stories/modals/ui-modal.story.md
Normal file
21
@xen-orchestra/lite/src/stories/modals/ui-modal.story.md
Normal file
@ -0,0 +1,21 @@
|
||||
This component only handle the modal backdrop and content positioning.
|
||||
|
||||
You can use any pre-made layouts, create your own or use the `ModalContainer` component.
|
||||
|
||||
It is meant to be used with `useModal` composable.
|
||||
|
||||
```vue-template
|
||||
<button @click="open">Delete all items</button>
|
||||
|
||||
<UiModal v-model="isOpen">
|
||||
<ModalContainer...>
|
||||
<!-- <ConfirmModalLayout ...> (Or you can use a pre-made layout) -->
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
import { faRemove } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useModal } from "@composable/modal.composable";
|
||||
|
||||
const { open, close, isOpen } = useModal().
|
||||
```
|
24
@xen-orchestra/lite/src/stories/modals/ui-modal.story.vue
Normal file
24
@xen-orchestra/lite/src/stories/modals/ui-modal.story.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
:params="[
|
||||
model()
|
||||
.required()
|
||||
.type('boolean')
|
||||
.help('Whether the modal is opened or not'),
|
||||
colorProp().ctx(),
|
||||
slot().help('Place your ModalContainer here'),
|
||||
]"
|
||||
>
|
||||
<button type="button" @click="open">Open modal</button>
|
||||
<UiModal v-model="isOpen" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiModal from "@/components/ui/modals/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { colorProp, model, slot } from "@/libs/story/story-param";
|
||||
|
||||
const { isOpen, open } = useModal();
|
||||
</script>
|
@ -1,19 +0,0 @@
|
||||
```vue-template
|
||||
<button @click="open">Delete all items</button>
|
||||
|
||||
<UiModal v-if="isOpen" @close="close" :icon="faRemove">
|
||||
<template #title>You are about to delete 12 items</template>
|
||||
<template #subtitle>They'll be gone forever</template>
|
||||
<template #buttons>
|
||||
<UiButton @click="delete" color="error">Yes, delete</UiButton>
|
||||
<UiButton @click="close">Cancel</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
import { faRemove } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useModal } from "@composable/modal.composable";
|
||||
|
||||
const { open, close, isOpen } = useModal().
|
||||
```
|
@ -47,3 +47,7 @@ export const IK_BUTTON_GROUP_TRANSPARENT = Symbol() as InjectionKey<
|
||||
export const IK_CARD_GROUP_VERTICAL = Symbol() as InjectionKey<boolean>;
|
||||
|
||||
export const IK_INPUT_ID = Symbol() as InjectionKey<ComputedRef<string>>;
|
||||
|
||||
export const IK_MODAL_CLOSE = Symbol() as InjectionKey<() => void>;
|
||||
|
||||
export const IK_MODAL_NESTED = Symbol() as InjectionKey<boolean>;
|
||||
|
Loading…
Reference in New Issue
Block a user