mirror of https://github.com/halo-dev/halo
refactor: improve ui/ux of attachment group and policy selector (#5996)
#### What type of PR is this? /area ui /kind improvement /milestone 2.16.x #### What this PR does / why we need it: 优化附件分组、存储策略选择组件的 UI。 <img width="1649" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/194d6334-e600-4235-85f7-affb1d44c003"> <img width="953" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/d8cb2470-2500-4b2c-a251-5fdf45a9cccb"> #### Does this PR introduce a user-facing change? ```release-note 优化附件分组、存储策略选择组件的 UI。 ```pull/5994/head^2
parent
19fb1c2311
commit
94826d44c0
|
@ -1,4 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { isImage } from "@/utils/image";
|
||||
import type { Attachment, Group } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
|
@ -20,25 +25,20 @@ import {
|
|||
VPagination,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
|
||||
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
|
||||
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
|
||||
import AttachmentGroupList from "./components/AttachmentGroupList.vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, onMounted, provide, ref, watch } from "vue";
|
||||
import type { Attachment, Group } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
|
||||
import { useAttachmentControl } from "./composables/use-attachment";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { isImage } from "@/utils/image";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
||||
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
|
||||
import AttachmentGroupList from "./components/AttachmentGroupList.vue";
|
||||
import AttachmentListItem from "./components/AttachmentListItem.vue";
|
||||
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
|
||||
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
|
||||
import { useAttachmentControl } from "./composables/use-attachment";
|
||||
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -47,7 +47,7 @@ const uploadVisible = ref(false);
|
|||
const detailVisible = ref(false);
|
||||
|
||||
const { policies } = useFetchAttachmentPolicy();
|
||||
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
|
||||
const { groups } = useFetchAttachmentGroup();
|
||||
|
||||
const selectedGroup = useRouteQuery<string | undefined>("group");
|
||||
|
||||
|
@ -465,11 +465,7 @@ onMounted(() => {
|
|||
</template>
|
||||
|
||||
<div :style="`${viewType === 'list' ? 'padding:12px 16px 0' : ''}`">
|
||||
<AttachmentGroupList
|
||||
@select="handleReset"
|
||||
@update="handleFetchGroups"
|
||||
@reload-attachments="handleFetchAttachments"
|
||||
/>
|
||||
<AttachmentGroupList @select="handleReset" />
|
||||
</div>
|
||||
|
||||
<VLoading v-if="isLoading" />
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
<script lang="ts" setup>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Group } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconCheckboxCircle,
|
||||
IconMore,
|
||||
Toast,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
group?: Group;
|
||||
isSelected?: boolean;
|
||||
features?: { actions: boolean; checkIcon?: boolean };
|
||||
}>(),
|
||||
{
|
||||
group: undefined,
|
||||
isSelected: false,
|
||||
features: () => {
|
||||
return {
|
||||
actions: true,
|
||||
checkIcon: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDelete = () => {
|
||||
Dialog.warning({
|
||||
title: t("core.attachment.group_list.operations.delete.title"),
|
||||
description: t("core.attachment.group_list.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
if (!props.group) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
fieldSelector: [`spec.groupName=${props.group.metadata.name}`],
|
||||
page: 0,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
await apiClient.extension.storage.group.deleteStorageHaloRunV1alpha1Group(
|
||||
{ name: props.group.metadata.name }
|
||||
);
|
||||
|
||||
// move attachments to none group
|
||||
const moveToUnGroupRequests = data.items.map((attachment) => {
|
||||
attachment.spec.groupName = undefined;
|
||||
return apiClient.extension.storage.attachment.updateStorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
attachment: attachment,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(moveToUnGroupRequests);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["attachment-groups"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["attachments"] });
|
||||
|
||||
Toast.success(
|
||||
t("core.attachment.group_list.operations.delete.toast_success", {
|
||||
total: data.total,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteWithAttachments = () => {
|
||||
Dialog.warning({
|
||||
title: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.title"
|
||||
),
|
||||
description: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.description"
|
||||
),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
if (!props.group) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
fieldSelector: [`spec.groupName=${props.group.metadata.name}`],
|
||||
page: 0,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
await apiClient.extension.storage.group.deleteStorageHaloRunV1alpha1Group(
|
||||
{ name: props.group.metadata.name }
|
||||
);
|
||||
|
||||
const deleteAttachmentRequests = data.items.map((attachment) => {
|
||||
return apiClient.extension.storage.attachment.deleteStorageHaloRunV1alpha1Attachment(
|
||||
{ name: attachment.metadata.name }
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deleteAttachmentRequests);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["attachment-groups"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["attachments"] });
|
||||
|
||||
Toast.success(
|
||||
t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.toast_success",
|
||||
{ total: data.total }
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Editing
|
||||
const editingModalVisible = ref(false);
|
||||
|
||||
const onEditingModalClose = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["attachment-groups"] });
|
||||
editingModalVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-full w-full items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50 hover:shadow-sm"
|
||||
:class="{ '!bg-gray-100 shadow-sm': isSelected }"
|
||||
>
|
||||
<div class="inline-flex w-full flex-1 gap-x-2 break-all text-left">
|
||||
<slot name="text">
|
||||
{{ group?.spec.displayName }}
|
||||
</slot>
|
||||
<VStatusDot
|
||||
v-if="group?.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<HasPermission
|
||||
v-if="features.actions"
|
||||
:permissions="['system:attachments:manage']"
|
||||
>
|
||||
<VDropdown>
|
||||
<IconMore @click.stop />
|
||||
<template #popper>
|
||||
<VDropdownItem @click="editingModalVisible = true">
|
||||
{{ $t("core.attachment.group_list.operations.rename.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdown placement="right" :triggers="['click']">
|
||||
<VDropdownItem type="danger">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
<template #popper>
|
||||
<VDropdownItem type="danger" @click="handleDelete()">
|
||||
{{
|
||||
$t("core.attachment.group_list.operations.delete.button")
|
||||
}}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem
|
||||
type="danger"
|
||||
@click="handleDeleteWithAttachments()"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.button"
|
||||
)
|
||||
}}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</HasPermission>
|
||||
|
||||
<IconCheckboxCircle
|
||||
v-if="isSelected && features.checkIcon"
|
||||
class="text-primary"
|
||||
/>
|
||||
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<AttachmentGroupEditingModal
|
||||
v-if="editingModalVisible"
|
||||
:group="group"
|
||||
@close="onEditingModalClose"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts" setup>
|
||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import type { Group } from "@halo-dev/api-client";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Group } from "@halo-dev/api-client";
|
||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -35,7 +35,7 @@ const formState = ref<Group>({
|
|||
generateName: "attachment-group-",
|
||||
},
|
||||
});
|
||||
const saving = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const modalTitle = props.group
|
||||
? t("core.attachment.group_editing_modal.titles.update")
|
||||
|
@ -43,7 +43,7 @@ const modalTitle = props.group
|
|||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
isSubmitting.value = true;
|
||||
if (props.group) {
|
||||
await apiClient.extension.storage.group.updateStorageHaloRunV1alpha1Group(
|
||||
{
|
||||
|
@ -64,7 +64,7 @@ const handleSave = async () => {
|
|||
} catch (e) {
|
||||
console.error("Failed to save attachment group", e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -77,7 +77,13 @@ onMounted(() => {
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<VModal ref="modal" :title="modalTitle" :width="500" @close="emit('close')">
|
||||
<VModal
|
||||
ref="modal"
|
||||
mount-to-body
|
||||
:title="modalTitle"
|
||||
:width="500"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<FormKit
|
||||
id="attachment-group-form"
|
||||
name="attachment-group-form"
|
||||
|
@ -100,7 +106,7 @@ onMounted(() => {
|
|||
<template #footer>
|
||||
<VSpace>
|
||||
<SubmitButton
|
||||
:loading="saving"
|
||||
:loading="isSubmitting"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('attachment-group-form')"
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { ref } from "vue";
|
||||
|
||||
// components
|
||||
import {
|
||||
Dialog,
|
||||
IconAddCircle,
|
||||
IconMore,
|
||||
Toast,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
||||
|
||||
// types
|
||||
import type { Group } from "@halo-dev/api-client";
|
||||
|
||||
import { IconAddCircle } from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import AttachmentGroupBadge from "./AttachmentGroupBadge.vue";
|
||||
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -35,10 +22,10 @@ const props = withDefaults(
|
|||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", group: Group): void;
|
||||
(event: "update"): void;
|
||||
(event: "reload-attachments"): void;
|
||||
}>();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const defaultGroups: Group[] = [
|
||||
{
|
||||
spec: {
|
||||
|
@ -62,11 +49,10 @@ const defaultGroups: Group[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
|
||||
const { groups } = useFetchAttachmentGroup();
|
||||
|
||||
const groupToUpdate = ref<Group>();
|
||||
const loading = ref<boolean>(false);
|
||||
const editingModal = ref(false);
|
||||
const creationModalVisible = ref(false);
|
||||
|
||||
const selectedGroup = props.readonly
|
||||
? ref("")
|
||||
|
@ -77,189 +63,52 @@ const handleSelectGroup = (group: Group) => {
|
|||
selectedGroup.value = group.metadata.name;
|
||||
};
|
||||
|
||||
const handleOpenEditingModal = (group: Group) => {
|
||||
groupToUpdate.value = group;
|
||||
editingModal.value = true;
|
||||
};
|
||||
|
||||
const onEditingModalClose = () => {
|
||||
emit("update");
|
||||
handleFetchGroups();
|
||||
editingModal.value = false;
|
||||
};
|
||||
|
||||
const handleDelete = (group: Group) => {
|
||||
Dialog.warning({
|
||||
title: t("core.attachment.group_list.operations.delete.title"),
|
||||
description: t("core.attachment.group_list.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
fieldSelector: [`spec.groupName=${group.metadata.name}`],
|
||||
page: 0,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
await apiClient.extension.storage.group.deleteStorageHaloRunV1alpha1Group(
|
||||
{ name: group.metadata.name }
|
||||
);
|
||||
|
||||
// move attachments to none group
|
||||
const moveToUnGroupRequests = data.items.map((attachment) => {
|
||||
attachment.spec.groupName = undefined;
|
||||
return apiClient.extension.storage.attachment.updateStorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
attachment: attachment,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(moveToUnGroupRequests);
|
||||
|
||||
handleFetchGroups();
|
||||
emit("reload-attachments");
|
||||
emit("update");
|
||||
|
||||
Toast.success(
|
||||
t("core.attachment.group_list.operations.delete.toast_success", {
|
||||
total: data.total,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteWithAttachments = (group: Group) => {
|
||||
Dialog.warning({
|
||||
title: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.title"
|
||||
),
|
||||
description: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.description"
|
||||
),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
fieldSelector: [`spec.groupName=${group.metadata.name}`],
|
||||
page: 0,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
await apiClient.extension.storage.group.deleteStorageHaloRunV1alpha1Group(
|
||||
{ name: group.metadata.name }
|
||||
);
|
||||
|
||||
const deleteAttachmentRequests = data.items.map((attachment) => {
|
||||
return apiClient.extension.storage.attachment.deleteStorageHaloRunV1alpha1Attachment(
|
||||
{ name: attachment.metadata.name }
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deleteAttachmentRequests);
|
||||
|
||||
handleFetchGroups();
|
||||
emit("reload-attachments");
|
||||
emit("update");
|
||||
|
||||
Toast.success(
|
||||
t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.toast_success",
|
||||
{ total: data.total }
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
const onCreationModalClose = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["attachment-groups"] });
|
||||
creationModalVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<AttachmentGroupEditingModal
|
||||
v-if="!readonly && editingModal"
|
||||
:group="groupToUpdate"
|
||||
@close="onEditingModalClose"
|
||||
v-if="!readonly && creationModalVisible"
|
||||
@close="onCreationModalClose"
|
||||
/>
|
||||
<div class="mb-5 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-6">
|
||||
<div
|
||||
<div
|
||||
class="mb-5 grid grid-cols-2 gap-x-2 gap-y-3 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6"
|
||||
>
|
||||
<AttachmentGroupBadge
|
||||
v-for="defaultGroup in defaultGroups"
|
||||
:key="defaultGroup.metadata.name"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900':
|
||||
defaultGroup.metadata.name === selectedGroup,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
:group="defaultGroup"
|
||||
:is-selected="defaultGroup.metadata.name === selectedGroup"
|
||||
:features="{ actions: false, checkIcon: readonly }"
|
||||
@click="handleSelectGroup(defaultGroup)"
|
||||
>
|
||||
<div class="flex flex-1 items-center">
|
||||
<span class="text-sm">{{ defaultGroup.spec.displayName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
/>
|
||||
|
||||
<AttachmentGroupBadge
|
||||
v-for="group in groups"
|
||||
:key="group.metadata.name"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900': group.metadata.name === selectedGroup,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
:group="group"
|
||||
:is-selected="group.metadata.name === selectedGroup"
|
||||
:features="{ actions: !readonly, checkIcon: readonly }"
|
||||
@click="handleSelectGroup(group)"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ group.spec.displayName }}
|
||||
</span>
|
||||
<VStatusDot
|
||||
v-if="group.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
<VDropdown v-if="!readonly" v-permission="['system:attachments:manage']">
|
||||
<IconMore @click.stop />
|
||||
<template #popper>
|
||||
<VDropdownItem @click="handleOpenEditingModal(group)">
|
||||
{{ $t("core.attachment.group_list.operations.rename.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdown placement="right" :triggers="['click']">
|
||||
<VDropdownItem type="danger">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
<template #popper>
|
||||
<VDropdownItem type="danger" @click="handleDelete(group)">
|
||||
{{ $t("core.attachment.group_list.operations.delete.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem
|
||||
type="danger"
|
||||
@click="handleDeleteWithAttachments(group)"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.button"
|
||||
)
|
||||
}}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
<div
|
||||
/>
|
||||
|
||||
<HasPermission
|
||||
v-if="!loading && !readonly"
|
||||
v-permission="['system:attachments:manage']"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
@click="editingModal = true"
|
||||
:permissions="['system:attachments:manage']"
|
||||
>
|
||||
<div class="flex flex-1 items-center truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</span>
|
||||
</div>
|
||||
<IconAddCircle />
|
||||
</div>
|
||||
<AttachmentGroupBadge
|
||||
:features="{ actions: false }"
|
||||
@click="creationModalVisible = true"
|
||||
>
|
||||
<template #text>
|
||||
<span>{{ $t("core.common.buttons.new") }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<IconAddCircle />
|
||||
</template>
|
||||
</AttachmentGroupBadge>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconAddCircle,
|
||||
|
@ -13,16 +16,13 @@ import {
|
|||
VSpace,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||
import { ref } from "vue";
|
||||
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
useFetchAttachmentPolicy,
|
||||
useFetchAttachmentPolicyTemplate,
|
||||
} from "../composables/use-attachment-policy";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
|
@ -63,7 +63,7 @@ const handleDelete = async (policy: Policy) => {
|
|||
"core.attachment.policies_modal.operations.can_not_delete.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
showCancel: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -88,9 +88,17 @@ const handleDelete = async (policy: Policy) => {
|
|||
|
||||
const onEditingModalClose = () => {
|
||||
selectedPolicy.value = undefined;
|
||||
selectedTemplateName.value = undefined;
|
||||
handleFetchPolicies();
|
||||
policyEditingModal.value = false;
|
||||
};
|
||||
|
||||
function getPolicyTemplateDisplayName(templateName: string) {
|
||||
const policyTemplate = policyTemplates.value?.find(
|
||||
(template) => template.metadata.name === templateName
|
||||
);
|
||||
return policyTemplate?.spec?.displayName || "--";
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
|
@ -108,8 +116,8 @@ const onEditingModalClose = () => {
|
|||
</span>
|
||||
<template #popper>
|
||||
<VDropdownItem
|
||||
v-for="(policyTemplate, index) in policyTemplates"
|
||||
:key="index"
|
||||
v-for="policyTemplate in policyTemplates"
|
||||
:key="policyTemplate.metadata.name"
|
||||
@click="handleOpenCreateNewPolicyModal(policyTemplate)"
|
||||
>
|
||||
{{ policyTemplate.spec?.displayName }}
|
||||
|
@ -157,7 +165,9 @@ const onEditingModalClose = () => {
|
|||
<template #start>
|
||||
<VEntityField
|
||||
:title="policy.spec.displayName"
|
||||
:description="policy.spec.templateName"
|
||||
:description="
|
||||
getPolicyTemplateDisplayName(policy.spec.templateName)
|
||||
"
|
||||
></VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Policy } from "@halo-dev/api-client";
|
||||
import { IconCheckboxCircle } from "@halo-dev/components";
|
||||
import { computed } from "vue";
|
||||
import { useFetchAttachmentPolicyTemplate } from "../composables/use-attachment-policy";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
policy?: Policy;
|
||||
isSelected?: boolean;
|
||||
features?: { checkIcon?: boolean };
|
||||
}>(),
|
||||
{
|
||||
policy: undefined,
|
||||
isSelected: false,
|
||||
features: () => {
|
||||
return {
|
||||
checkIcon: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { policyTemplates } = useFetchAttachmentPolicyTemplate();
|
||||
|
||||
const policyTemplate = computed(() => {
|
||||
return policyTemplates.value?.find(
|
||||
(template) => template.metadata.name === props.policy?.spec.templateName
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-full w-full items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50 hover:shadow-sm"
|
||||
:class="{ '!bg-gray-100 shadow-sm': isSelected }"
|
||||
>
|
||||
<div class="inline-flex w-full flex-1 flex-col space-y-0.5 text-left">
|
||||
<slot name="text">
|
||||
<div>
|
||||
{{ policy?.spec.displayName }}
|
||||
</div>
|
||||
<div class="text-xs font-normal text-gray-600">
|
||||
{{ policyTemplate?.spec?.displayName || "--" }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="flex-none">
|
||||
<IconCheckboxCircle
|
||||
v-if="isSelected && features.checkIcon"
|
||||
class="text-primary"
|
||||
/>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
|
@ -1,14 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { Toast, VButton, VLoading, VModal, VSpace } from "@halo-dev/components";
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||
import type { Policy } from "@halo-dev/api-client";
|
||||
import { Toast, VButton, VLoading, VModal, VSpace } from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { computed, onMounted, ref, toRaw, toRefs } from "vue";
|
||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -63,6 +63,7 @@ const { data: policyTemplate } = useQuery({
|
|||
"core:attachment:policy-template",
|
||||
formState.value.spec.templateName,
|
||||
],
|
||||
cacheTime: 0,
|
||||
queryFn: async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.storage.policyTemplate.getStorageHaloRunV1alpha1PolicyTemplate(
|
||||
|
@ -81,6 +82,7 @@ const { data: setting, isLoading } = useQuery({
|
|||
"core:attachment:policy-template:setting",
|
||||
policyTemplate.value?.spec?.settingName,
|
||||
],
|
||||
cacheTime: 0,
|
||||
queryFn: async () => {
|
||||
if (!policyTemplate.value?.spec?.settingName) {
|
||||
throw new Error("No setting found");
|
||||
|
@ -101,6 +103,7 @@ const { data: configMap } = useQuery({
|
|||
"core:attachment:policy-template:configMap",
|
||||
policy.value?.spec.configMapName,
|
||||
],
|
||||
cacheTime: 0,
|
||||
initialData: {
|
||||
data: {},
|
||||
apiVersion: "v1alpha1",
|
||||
|
@ -181,7 +184,13 @@ const modalTitle = props.policy
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<VModal ref="modal" :title="modalTitle" :width="600" @close="emit('close')">
|
||||
<VModal
|
||||
ref="modal"
|
||||
mount-to-body
|
||||
:title="modalTitle"
|
||||
:width="600"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div>
|
||||
<VLoading v-if="isLoading" />
|
||||
<template v-else>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import type { PolicyTemplate } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconAddCircle,
|
||||
VAlert,
|
||||
|
@ -6,15 +7,16 @@ import {
|
|||
VDropdownItem,
|
||||
VModal,
|
||||
} from "@halo-dev/components";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { ref, watch } from "vue";
|
||||
import type { PolicyTemplate } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import {
|
||||
useFetchAttachmentPolicy,
|
||||
useFetchAttachmentPolicyTemplate,
|
||||
} from "../composables/use-attachment-policy";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import AttachmentGroupBadge from "./AttachmentGroupBadge.vue";
|
||||
import AttachmentPolicyBadge from "./AttachmentPolicyBadge.vue";
|
||||
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
|
@ -67,6 +69,7 @@ const handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {
|
|||
|
||||
const onEditingModalClose = async () => {
|
||||
await handleFetchPolicies();
|
||||
policyTemplateNameToCreate.value = undefined;
|
||||
selectedPolicyName.value = policies.value?.[0].metadata.name;
|
||||
policyEditingModal.value = false;
|
||||
};
|
||||
|
@ -75,80 +78,37 @@ const onEditingModalClose = async () => {
|
|||
<VModal
|
||||
ref="modal"
|
||||
:body-class="['!p-0']"
|
||||
:width="650"
|
||||
:centered="false"
|
||||
:width="920"
|
||||
height="calc(100vh - 20px)"
|
||||
:title="$t('core.attachment.upload_modal.title')"
|
||||
mount-to-body
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="w-full p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">
|
||||
{{ $t("core.attachment.upload_modal.filters.group.label") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(group, index) in [
|
||||
{
|
||||
metadata: { name: '' },
|
||||
spec: {
|
||||
displayName: $t('core.attachment.common.text.ungrouped'),
|
||||
},
|
||||
},
|
||||
...(groups || []),
|
||||
]"
|
||||
:key="index"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900':
|
||||
group.metadata.name === selectedGroupName,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
@click="selectedGroupName = group.metadata.name"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ group.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">
|
||||
{{ $t("core.attachment.upload_modal.filters.policy.label") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(policy, index) in policies"
|
||||
:key="index"
|
||||
:class="{
|
||||
'!bg-gray-200 !text-gray-900':
|
||||
selectedPolicyName === policy.metadata.name,
|
||||
}"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
<AttachmentPolicyBadge
|
||||
v-for="policy in policies"
|
||||
:key="policy.metadata.name"
|
||||
:policy="policy"
|
||||
:is-selected="selectedPolicyName === policy.metadata.name"
|
||||
:features="{ checkIcon: true }"
|
||||
@click="selectedPolicyName = policy.metadata.name"
|
||||
>
|
||||
<div class="flex flex-1 flex-col items-start truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ policy.spec.displayName }}
|
||||
</span>
|
||||
<span class="text-xs">
|
||||
{{ policy.spec.templateName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<VDropdown>
|
||||
<div
|
||||
class="flex h-full cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
>
|
||||
<div class="flex flex-1 items-center truncate">
|
||||
<span class="truncate text-sm">
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</span>
|
||||
</div>
|
||||
<IconAddCircle />
|
||||
</div>
|
||||
<AttachmentPolicyBadge>
|
||||
<template #text>
|
||||
<span>{{ $t("core.common.buttons.new") }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<IconAddCircle />
|
||||
</template>
|
||||
</AttachmentPolicyBadge>
|
||||
<template #popper>
|
||||
<VDropdownItem
|
||||
v-for="(policyTemplate, index) in policyTemplates"
|
||||
|
@ -169,6 +129,32 @@ const onEditingModalClose = async () => {
|
|||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">
|
||||
{{ $t("core.attachment.upload_modal.filters.group.label") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<AttachmentGroupBadge
|
||||
v-for="group in [
|
||||
{
|
||||
metadata: { name: '' },
|
||||
apiVersion: '',
|
||||
kind: '',
|
||||
spec: {
|
||||
displayName: $t('core.attachment.common.text.ungrouped'),
|
||||
},
|
||||
},
|
||||
...(groups || []),
|
||||
]"
|
||||
:key="group.metadata.name"
|
||||
:group="group"
|
||||
:is-selected="group.metadata.name === selectedGroupName"
|
||||
:features="{ actions: false, checkIcon: true }"
|
||||
@click="selectedGroupName = group.metadata.name"
|
||||
>
|
||||
</AttachmentGroupBadge>
|
||||
</div>
|
||||
<UppyUpload
|
||||
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
|
||||
:disabled="!selectedPolicyName"
|
||||
|
@ -176,6 +162,7 @@ const onEditingModalClose = async () => {
|
|||
policyName: selectedPolicyName,
|
||||
groupName: selectedGroupName,
|
||||
}"
|
||||
width="100%"
|
||||
:allowed-meta-fields="['policyName', 'groupName']"
|
||||
:note="
|
||||
selectedPolicyName
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import { isImage } from "@/utils/image";
|
||||
import { matchMediaTypes } from "@/utils/media-type";
|
||||
import type { Attachment, Group } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
|
@ -12,16 +16,12 @@ import {
|
|||
VPagination,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { isImage } from "@/utils/image";
|
||||
import { useAttachmentControl } from "../../composables/use-attachment";
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import type { AttachmentLike } from "@halo-dev/console-shared";
|
||||
import type { Attachment, Group } from "@halo-dev/api-client";
|
||||
import AttachmentUploadModal from "../AttachmentUploadModal.vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useAttachmentControl } from "../../composables/use-attachment";
|
||||
import AttachmentDetailModal from "../AttachmentDetailModal.vue";
|
||||
import AttachmentGroupList from "../AttachmentGroupList.vue";
|
||||
import { matchMediaTypes } from "@/utils/media-type";
|
||||
import AttachmentUploadModal from "../AttachmentUploadModal.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
Loading…
Reference in New Issue