refactor: improve code base of attachment-related (#5967)

#### What type of PR is this?

/area ui
/kind improvement
/milestone 2.16.x

#### What this PR does / why we need it:

优化附件管理的相关代码。

1. 优化附件管理相关的对话框组件,减少重复和不必要的请求。
2. 简化附件存储策略编辑组件的逻辑。

#### Special notes for your reviewer:

需要测试:

1. 附件上传的相关功能。
2. 附件存储策略的编辑和新建。
3. 附件筛选条件功能。

#### Does this PR introduce a user-facing change?

```release-note
优化附件管理相关代码,减少重复和不必要的请求。
```
pull/5975/head
Ryan Wang 2024-05-23 10:54:49 +08:00 committed by GitHub
parent ce5757ae10
commit 89b20bf5a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 278 additions and 631 deletions

View File

@ -2,185 +2,11 @@
// types
import { computed, watch, type ComputedRef, type Ref } from "vue";
import { ref } from "vue";
import { apiClient } from "@/utils/api-client";
// libs
import { cloneDeep, merge } from "lodash-es";
import { cloneDeep } from "lodash-es";
import type { ConfigMap, Setting, SettingForm } from "@halo-dev/api-client";
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
import { Toast } from "@halo-dev/components";
import { useI18n } from "vue-i18n";
const initialConfigMap: ConfigMap = {
apiVersion: "v1alpha1",
kind: "ConfigMap",
metadata: {
name: "",
},
data: {},
};
interface useSettingFormReturn {
setting: Ref<Setting | undefined>;
configMap: Ref<ConfigMap>;
configMapFormData: Ref<Record<string, Record<string, string>> | undefined>;
saving: Ref<boolean>;
handleFetchSettings: () => void;
handleFetchConfigMap: () => void;
handleSaveConfigMap: () => void;
handleReset: () => void;
}
export function useSettingForm(
settingName: Ref<string | undefined>,
configMapName: Ref<string | undefined>
): useSettingFormReturn {
const { t } = useI18n();
const setting = ref<Setting>();
const configMap = ref<ConfigMap>(cloneDeep(initialConfigMap));
const configMapFormData = ref<
Record<string, Record<string, string>> | undefined
>();
const saving = ref(false);
const handleFetchSettings = async () => {
if (!settingName.value) {
setting.value = undefined;
return;
}
try {
const { data } = await apiClient.extension.setting.getv1alpha1Setting({
name: settingName.value,
});
setting.value = data;
// init configMapFormData
if (!configMapFormData.value) {
const { forms } = setting.value.spec;
const initialConfigMapFormData: Record<
string,
Record<string, string>
> = {};
forms.forEach((form) => {
initialConfigMapFormData[form.group] = {};
const formSchema = form.formSchema as (
| FormKitSchemaCondition
| FormKitSchemaNode
)[];
formSchema.forEach((schema) => {
// @ts-ignore
if ("name" in schema && "$formkit" in schema) {
initialConfigMapFormData[form.group][schema.name] =
schema.value || undefined;
}
});
});
configMapFormData.value = cloneDeep(initialConfigMapFormData);
}
} catch (e) {
console.error("Failed to fetch setting", e);
}
};
const handleFetchConfigMap = async () => {
if (!configMapName.value) {
configMap.value = cloneDeep(initialConfigMap);
configMapFormData.value = undefined;
return;
}
try {
const response = await apiClient.extension.configMap.getv1alpha1ConfigMap(
{
name: configMapName.value,
},
{
mute: true,
}
);
configMap.value = response.data;
const { data } = configMap.value;
if (data) {
// merge objects value
const { forms } = setting.value?.spec || {};
forms?.forEach((form) => {
if (!configMapFormData.value) {
return;
}
configMapFormData.value[form.group] = merge(
configMapFormData.value[form.group] || {},
JSON.parse(data[form.group] || "{}")
);
});
}
} catch (e) {
console.error("Failed to fetch configMap", e);
}
};
const handleSaveConfigMap = async () => {
try {
saving.value = true;
if (!configMap.value.metadata.name && configMapName.value) {
configMap.value.metadata.name = configMapName.value;
}
setting.value?.spec.forms.forEach((item: SettingForm) => {
// @ts-ignore
configMap.value.data[item.group] = JSON.stringify(
configMapFormData?.value?.[item.group]
);
});
if (!configMap.value.metadata.creationTimestamp) {
const { data } =
await apiClient.extension.configMap.createv1alpha1ConfigMap({
configMap: configMap.value,
});
configMapName.value = data.metadata.name;
} else {
const { data } =
await apiClient.extension.configMap.updatev1alpha1ConfigMap({
configMap: configMap.value,
name: configMap.value.metadata.name,
});
configMapName.value = data.metadata.name;
}
Toast.success(t("core.common.toast.save_success"));
} catch (e) {
console.error("Failed to save configMap", e);
} finally {
await handleFetchSettings();
await handleFetchConfigMap();
saving.value = false;
}
};
const handleReset = () => {
setting.value = undefined;
configMap.value = cloneDeep(initialConfigMap);
configMapFormData.value = undefined;
};
return {
setting,
configMap,
configMapFormData,
saving,
handleFetchSettings,
handleFetchConfigMap,
handleSaveConfigMap,
handleReset,
};
}
interface useSettingFormConvertReturn {
formSchema: ComputedRef<

View File

@ -4,28 +4,29 @@ import {
IconArrowRight,
IconCheckboxFill,
IconDatabase2Line,
IconFolder,
IconGrid,
IconList,
IconUpload,
IconRefreshLine,
IconUpload,
Toast,
VButton,
VCard,
VDropdown,
VDropdownItem,
VEmpty,
VLoading,
VPageHeader,
VPagination,
VSpace,
VEmpty,
IconFolder,
VLoading,
Toast,
VDropdown,
VDropdownItem,
} 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 { computed, onMounted, ref, watch } from "vue";
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";
@ -37,8 +38,6 @@ 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 { provide } from "vue";
import type { Ref } from "vue";
import AttachmentListItem from "./components/AttachmentListItem.vue";
const { t } = useI18n();
@ -50,7 +49,7 @@ const detailVisible = ref(false);
const { policies } = useFetchAttachmentPolicy();
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
const selectedGroup = ref<Group>();
const selectedGroup = useRouteQuery<string | undefined>("group");
// Filter
const keyword = useRouteQuery<string>("keyword", "");
@ -111,12 +110,8 @@ const {
isChecked,
handleReset,
} = useAttachmentControl({
group: selectedGroup,
policy: computed(() => {
return policies.value?.find(
(policy) => policy.metadata.name === selectedPolicy.value
);
}),
groupName: selectedGroup,
policyName: selectedPolicy,
user: selectedUser,
accepts: computed(() => {
if (!selectedAccepts.value) {
@ -187,6 +182,7 @@ const onDetailModalClose = () => {
const onUploadModalClose = () => {
routeQueryAction.value = undefined;
handleFetchAttachments();
uploadVisible.value = false;
};
// View type
@ -258,11 +254,11 @@ onMounted(() => {
</span>
</template>
</AttachmentDetailModal>
<AttachmentUploadModal
v-model:visible="uploadVisible"
@close="onUploadModalClose"
<AttachmentUploadModal v-if="uploadVisible" @close="onUploadModalClose" />
<AttachmentPoliciesModal
v-if="policyVisible"
@close="policyVisible = false"
/>
<AttachmentPoliciesModal v-model:visible="policyVisible" />
<VPageHeader :title="$t('core.attachment.title')">
<template #icon>
<IconFolder class="mr-2 self-center" />
@ -470,7 +466,6 @@ onMounted(() => {
<div :style="`${viewType === 'list' ? 'padding:12px 16px 0' : ''}`">
<AttachmentGroupList
v-model:selected-group="selectedGroup"
@select="handleReset"
@update="handleFetchGroups"
@reload-attachments="handleFetchAttachments"

View File

@ -2,32 +2,29 @@
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 { computed, ref, watch } from "vue";
import { cloneDeep } from "lodash-es";
import { onMounted, ref } from "vue";
import { apiClient } from "@/utils/api-client";
import { reset } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useI18n } from "vue-i18n";
import { cloneDeep } from "lodash-es";
const props = withDefaults(
defineProps<{
visible: boolean;
group: Group | null;
group?: Group;
}>(),
{
visible: false,
group: null,
group: undefined,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
const { t } = useI18n();
const initialFormState: Group = {
const modal = ref();
const formState = ref<Group>({
spec: {
displayName: "",
},
@ -37,25 +34,17 @@ const initialFormState: Group = {
name: "",
generateName: "attachment-group-",
},
};
const formState = ref<Group>(cloneDeep(initialFormState));
});
const saving = ref(false);
const isUpdateMode = computed(() => {
return !!formState.value.metadata.creationTimestamp;
});
const modalTitle = computed(() => {
return isUpdateMode.value
const modalTitle = props.group
? t("core.attachment.group_editing_modal.titles.update")
: t("core.attachment.group_editing_modal.titles.create");
});
const handleSave = async () => {
try {
saving.value = true;
if (isUpdateMode.value) {
if (props.group) {
await apiClient.extension.storage.group.updatestorageHaloRunV1alpha1Group(
{
name: formState.value.metadata.name,
@ -71,7 +60,7 @@ const handleSave = async () => {
}
Toast.success(t("core.common.toast.save_success"));
onVisibleChange(false);
modal.value.close();
} catch (e) {
console.error("Failed to save attachment group", e);
} finally {
@ -79,47 +68,16 @@ const handleSave = async () => {
}
};
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
reset("attachment-group-form");
};
watch(
() => props.visible,
(visible) => {
if (visible) {
onMounted(() => {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
);
watch(
() => props.group,
(group) => {
if (group) {
formState.value = cloneDeep(group);
} else {
handleResetForm();
if (props.group) {
formState.value = cloneDeep(props.group);
}
}
);
});
</script>
<template>
<VModal
:title="modalTitle"
:visible="visible"
:width="500"
@update:visible="onVisibleChange"
>
<VModal ref="modal" :title="modalTitle" :width="500" @close="emit('close')">
<FormKit
id="attachment-group-form"
name="attachment-group-form"
@ -142,14 +100,13 @@ watch(
<template #footer>
<VSpace>
<SubmitButton
v-if="visible"
:loading="saving"
type="secondary"
:text="$t('core.common.buttons.submit')"
@submit="$formkit.submit('attachment-group-form')"
>
</SubmitButton>
<VButton @click="onVisibleChange(false)">
<VButton @click="modal.close()">
{{ $t("core.common.buttons.cancel_and_shortcut") }}
</VButton>
</VSpace>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
// core libs
import { onMounted, ref, watch } from "vue";
import { ref } from "vue";
// components
import {
@ -26,17 +26,14 @@ const { t } = useI18n();
const props = withDefaults(
defineProps<{
selectedGroup: Group | undefined;
readonly?: boolean;
}>(),
{
selectedGroup: undefined,
readonly: false,
}
);
const emit = defineEmits<{
(event: "update:selectedGroup", group: Group): void;
(event: "select", group: Group): void;
(event: "update"): void;
(event: "reload-attachments"): void;
@ -67,19 +64,17 @@ const defaultGroups: Group[] = [
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
const groupToUpdate = ref<Group | null>(null);
const groupToUpdate = ref<Group>();
const loading = ref<boolean>(false);
const editingModal = ref(false);
const routeQuery = useRouteQuery<string>("group");
const selectedGroup = props.readonly
? ref("")
: useRouteQuery<string>("group", "");
const handleSelectGroup = (group: Group) => {
emit("update:selectedGroup", group);
emit("select", group);
if (!props.readonly) {
routeQuery.value = group.metadata.name;
}
selectedGroup.value = group.metadata.name;
};
const handleOpenEditingModal = (group: Group) => {
@ -90,6 +85,7 @@ const handleOpenEditingModal = (group: Group) => {
const onEditingModalClose = () => {
emit("update");
handleFetchGroups();
editingModal.value = false;
};
const handleDelete = (group: Group) => {
@ -181,51 +177,20 @@ const handleDeleteWithAttachments = (group: Group) => {
},
});
};
watch(
() => groups.value?.length,
() => {
const allGroups = [...defaultGroups, ...(groups.value || [])];
const groupIndex = allGroups.findIndex(
(group) => group.metadata.name === routeQuery.value
);
if (groupIndex < 0) {
handleSelectGroup(defaultGroups[0]);
}
}
);
onMounted(async () => {
await handleFetchGroups();
if (routeQuery.value && !props.readonly) {
const allGroups = [...defaultGroups, ...(groups.value || [])];
const group = allGroups.find(
(group) => group.metadata.name === routeQuery.value
);
if (group) {
handleSelectGroup(group);
return;
}
}
handleSelectGroup(defaultGroups[0]);
});
</script>
<template>
<AttachmentGroupEditingModal
v-if="!readonly"
v-model:visible="editingModal"
v-if="!readonly && editingModal"
:group="groupToUpdate"
@close="onEditingModalClose"
/>
<div class="mb-5 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-6">
<div
v-for="(defaultGroup, index) in defaultGroups"
:key="index"
v-for="defaultGroup in defaultGroups"
:key="defaultGroup.metadata.name"
:class="{
'!bg-gray-200 !text-gray-900':
defaultGroup.metadata.name === selectedGroup?.metadata.name,
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"
@click="handleSelectGroup(defaultGroup)"
@ -235,11 +200,10 @@ onMounted(async () => {
</div>
</div>
<div
v-for="(group, index) in groups"
:key="index"
v-for="group in groups"
:key="group.metadata.name"
:class="{
'!bg-gray-200 !text-gray-900':
group.metadata.name === selectedGroup?.metadata.name,
'!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"
@click="handleSelectGroup(group)"

View File

@ -1,15 +1,16 @@
<script lang="ts" setup>
import {
VSpace,
VStatusDot,
Dialog,
Toast,
VDropdownDivider,
VDropdownItem,
VEntity,
VEntityField,
Toast,
VDropdownItem,
Dialog,
VDropdownDivider,
VSpace,
VStatusDot,
} from "@halo-dev/components";
import { computed, ref } from "vue";
import type { Ref } from "vue";
import { computed, inject, markRaw, ref, toRefs } from "vue";
import type { Attachment } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import prettyBytes from "pretty-bytes";
@ -17,14 +18,10 @@ import { useFetchAttachmentPolicy } from "../composables/use-attachment-policy";
import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n";
import { inject } from "vue";
import type { Ref } from "vue";
import { useQueryClient } from "@tanstack/vue-query";
import { useOperationItemExtensionPoint } from "@console/composables/use-operation-extension-points";
import { toRefs } from "vue";
import type { OperationItem } from "@halo-dev/console-shared";
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
import { markRaw } from "vue";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -52,7 +49,7 @@ const selectedAttachments = inject<Ref<Set<Attachment>>>(
ref<Set<Attachment>>(new Set())
);
const policyName = computed(() => {
const policyDisplayName = computed(() => {
const policy = policies.value?.find(
(p) => p.metadata.name === props.attachment.spec.policyName
);
@ -180,7 +177,7 @@ const { operationItems } = useOperationItemExtensionPoint<Attachment>(
</VEntityField>
</template>
<template #end>
<VEntityField :description="policyName" />
<VEntityField :description="policyDisplayName" />
<VEntityField>
<template #description>
<RouterLink

View File

@ -2,16 +2,14 @@
import { VButton } from "@halo-dev/components";
import type { Attachment } from "@halo-dev/api-client";
import { useAttachmentPermalinkCopy } from "../composables/use-attachment";
import { toRefs, computed } from "vue";
import { computed, toRefs } from "vue";
const props = withDefaults(
defineProps<{
visible: boolean;
attachment?: Attachment;
mountToBody?: boolean;
}>(),
{
visible: false,
attachment: undefined,
mountToBody: false,
}

View File

@ -1,17 +1,17 @@
<script lang="ts" setup>
import {
IconAddCircle,
VButton,
VModal,
VSpace,
VEmpty,
Dialog,
VEntity,
VEntityField,
VStatusDot,
IconAddCircle,
Toast,
VButton,
VDropdown,
VDropdownItem,
Toast,
VEmpty,
VEntity,
VEntityField,
VModal,
VSpace,
VStatusDot,
} from "@halo-dev/components";
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
import { ref } from "vue";
@ -24,17 +24,7 @@ import {
import { apiClient } from "@/utils/api-client";
import { useI18n } from "vue-i18n";
withDefaults(
defineProps<{
visible: boolean;
}>(),
{
visible: false,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
@ -43,36 +33,19 @@ const { t } = useI18n();
const { policies, isLoading, handleFetchPolicies } = useFetchAttachmentPolicy();
const { policyTemplates } = useFetchAttachmentPolicyTemplate();
const modal = ref();
const selectedPolicy = ref<Policy>();
const selectedTemplateName = ref();
const policyEditingModal = ref(false);
function onVisibleChange(visible: boolean) {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
}
const handleOpenEditingModal = (policy: Policy) => {
selectedPolicy.value = policy;
policyEditingModal.value = true;
};
const handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {
selectedPolicy.value = {
spec: {
displayName: "",
templateName: policyTemplate.metadata.name,
configMapName: "",
},
apiVersion: "storage.halo.run/v1alpha1",
kind: "Policy",
metadata: {
name: "",
generateName: "attachment-policy-",
},
};
selectedTemplateName.value = policyTemplate.metadata.name;
policyEditingModal.value = true;
};
@ -116,16 +89,17 @@ const handleDelete = async (policy: Policy) => {
const onEditingModalClose = () => {
selectedPolicy.value = undefined;
handleFetchPolicies();
policyEditingModal.value = false;
};
</script>
<template>
<VModal
:visible="visible"
ref="modal"
:width="750"
:title="$t('core.attachment.policies_modal.title')"
:body-class="['!p-0']"
:layer-closable="true"
@update:visible="onVisibleChange"
@close="emit('close')"
>
<template #actions>
<VDropdown>
@ -216,16 +190,16 @@ const onEditingModalClose = () => {
</li>
</ul>
<template #footer>
<VButton @click="onVisibleChange(false)">
<VButton @click="modal.close()">
{{ $t("core.common.buttons.close_and_shortcut") }}
</VButton>
</template>
</VModal>
<AttachmentPolicyEditingModal
v-if="visible"
v-model:visible="policyEditingModal"
v-if="policyEditingModal"
:policy="selectedPolicy"
:template-name="selectedTemplateName"
@close="onEditingModalClose"
/>
</template>

View File

@ -1,38 +1,37 @@
<script lang="ts" setup>
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import { Toast, VButton, VLoading, VModal, VSpace } from "@halo-dev/components";
import SubmitButton from "@/components/button/SubmitButton.vue";
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
import type { Policy } from "@halo-dev/api-client";
import { cloneDeep } from "lodash-es";
import { computed, ref, toRaw, watch, watchEffect } from "vue";
import { useSettingForm } from "@console/composables/use-setting-form";
import { computed, onMounted, ref, toRaw, toRefs } from "vue";
import { useSettingFormConvert } from "@console/composables/use-setting-form";
import { apiClient } from "@/utils/api-client";
import {
reset,
type FormKitSchemaCondition,
type FormKitSchemaNode,
} from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useI18n } from "vue-i18n";
import { useQuery } from "@tanstack/vue-query";
const props = withDefaults(
defineProps<{
visible: boolean;
policy?: Policy;
templateName?: string;
}>(),
{
visible: false,
policy: undefined,
templateName: undefined,
}
);
const { policy } = toRefs(props);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
const { t } = useI18n();
const initialFormState: Policy = {
const modal = ref();
const formState = ref<Policy>({
spec: {
displayName: "",
templateName: "",
@ -44,82 +43,106 @@ const initialFormState: Policy = {
name: "",
generateName: "attachment-policy-",
},
};
const formState = ref<Policy>(cloneDeep(initialFormState));
const policyTemplate = ref<PolicyTemplate | undefined>();
const settingName = computed(() => policyTemplate.value?.spec?.settingName);
const configMapName = computed({
get() {
return formState.value.spec.configMapName;
},
set(value) {
formState.value.spec.configMapName = value;
},
});
const {
const isUpdateMode = !!props.policy;
onMounted(async () => {
if (props.policy) {
formState.value = cloneDeep(props.policy);
}
if (props.templateName) {
formState.value.spec.templateName = props.templateName;
}
setFocus("displayNameInput");
});
const { data: policyTemplate } = useQuery({
queryKey: [
"core:attachment:policy-template",
formState.value.spec.templateName,
],
queryFn: async () => {
const { data } =
await apiClient.extension.storage.policyTemplate.getstorageHaloRunV1alpha1PolicyTemplate(
{
name: formState.value.spec.templateName,
}
);
return data;
},
retry: 0,
enabled: computed(() => !!formState.value.spec.templateName),
});
const { data: setting, isLoading } = useQuery({
queryKey: [
"core:attachment:policy-template:setting",
policyTemplate.value?.spec?.settingName,
],
queryFn: async () => {
if (!policyTemplate.value?.spec?.settingName) {
throw new Error("No setting found");
}
const { data } = await apiClient.extension.setting.getv1alpha1Setting({
name: policyTemplate.value.spec.settingName,
});
return data;
},
retry: 0,
enabled: computed(() => !!policyTemplate.value?.spec?.settingName),
});
const { data: configMap } = useQuery({
queryKey: [
"core:attachment:policy-template:configMap",
policy.value?.spec.configMapName,
],
initialData: {
data: {},
apiVersion: "v1alpha1",
kind: "ConfigMap",
metadata: {
generateName: "configMap-",
name: "",
},
},
retry: 0,
queryFn: async () => {
if (!policy.value?.spec.configMapName) {
throw new Error("No configMap found");
}
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap({
name: policy.value?.spec.configMapName,
});
return data;
},
enabled: computed(() => !!policy.value?.spec.configMapName),
});
const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
setting,
configMapFormData,
configMap,
saving,
handleFetchConfigMap,
handleFetchSettings,
handleSaveConfigMap,
handleReset: handleResetSettingForm,
} = useSettingForm(settingName, configMapName);
ref("default")
);
const formSchema = computed(() => {
if (!setting.value) {
return undefined;
}
const { forms } = setting.value.spec;
return forms.find((item) => item.group === "default")?.formSchema as (
| FormKitSchemaCondition
| FormKitSchemaNode
)[];
});
watchEffect(() => {
if (settingName.value) {
handleFetchSettings();
}
});
watchEffect(() => {
if (configMapName.value && setting.value) {
handleFetchConfigMap();
}
});
const isUpdateMode = computed(() => {
return !!formState.value.metadata.creationTimestamp;
});
const modalTitle = computed(() => {
return isUpdateMode.value
? t("core.attachment.policy_editing_modal.titles.update", {
policy: props.policy?.spec.displayName,
})
: t("core.attachment.policy_editing_modal.titles.create", {
policy_template: policyTemplate.value?.spec?.displayName,
});
});
const submitting = ref(false);
const handleSave = async () => {
try {
saving.value = true;
submitting.value = true;
if (!isUpdateMode.value) {
configMap.value.metadata.name = "";
configMap.value.metadata.generateName = "configMap-";
}
const configMapToUpdate = convertToSave();
await handleSaveConfigMap();
if (isUpdateMode) {
await apiClient.extension.configMap.updatev1alpha1ConfigMap({
name: configMap.value.metadata.name,
configMap: configMapToUpdate,
});
if (isUpdateMode.value) {
await apiClient.extension.storage.policy.updatestorageHaloRunV1alpha1Policy(
{
name: formState.value.metadata.name,
@ -127,7 +150,12 @@ const handleSave = async () => {
}
);
} else {
formState.value.spec.configMapName = configMap.value.metadata.name;
const { data: newConfigMap } =
await apiClient.extension.configMap.createv1alpha1ConfigMap({
configMap: configMapToUpdate,
});
formState.value.spec.configMapName = newConfigMap.metadata.name;
await apiClient.extension.storage.policy.createstorageHaloRunV1alpha1Policy(
{
policy: formState.value,
@ -136,78 +164,27 @@ const handleSave = async () => {
}
Toast.success(t("core.common.toast.save_success"));
onVisibleChange(false);
modal.value.close();
} catch (e) {
console.error("Failed to save attachment policy", e);
} finally {
saving.value = false;
submitting.value = false;
}
};
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
reset("attachment-policy-form");
};
watch(
() => props.visible,
(visible) => {
if (visible) {
setFocus("displayNameInput");
} else {
const timer = setTimeout(() => {
policyTemplate.value = undefined;
handleResetForm();
handleResetSettingForm();
clearTimeout(timer);
}, 100);
}
}
);
watch(
() => props.policy,
async (policy) => {
if (policy) {
formState.value = cloneDeep(policy);
const { templateName } = formState.value.spec;
// Get policy template
if (templateName) {
const { data } =
await apiClient.extension.storage.policyTemplate.getstorageHaloRunV1alpha1PolicyTemplate(
{
name: templateName,
}
);
policyTemplate.value = data;
}
} else {
setTimeout(() => {
policyTemplate.value = undefined;
handleResetForm();
handleResetSettingForm();
}, 100);
}
}
);
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const modalTitle = props.policy
? t("core.attachment.policy_editing_modal.titles.update", {
policy: props.policy?.spec.displayName,
})
: t("core.attachment.policy_editing_modal.titles.create", {
policy_template: policyTemplate.value?.spec?.displayName,
});
</script>
<template>
<VModal
:title="modalTitle"
:visible="visible"
:width="600"
@update:visible="onVisibleChange"
>
<VModal ref="modal" :title="modalTitle" :width="600" @close="emit('close')">
<div>
<VLoading v-if="isLoading" />
<template v-else>
<FormKit
v-if="formSchema && configMapFormData"
id="attachment-policy-form"
@ -223,7 +200,9 @@ const onVisibleChange = (visible: boolean) => {
id="displayNameInput"
v-model="formState.spec.displayName"
:label="
$t('core.attachment.policy_editing_modal.fields.display_name.label')
$t(
'core.attachment.policy_editing_modal.fields.display_name.label'
)
"
type="text"
name="displayName"
@ -234,19 +213,19 @@ const onVisibleChange = (visible: boolean) => {
:data="configMapFormData['default']"
/>
</FormKit>
</template>
</div>
<template #footer>
<VSpace>
<SubmitButton
v-if="visible"
:loading="saving"
:loading="submitting"
type="secondary"
:text="$t('core.common.buttons.submit')"
@submit="$formkit.submit('attachment-policy-form')"
>
</SubmitButton>
<VButton @click="onVisibleChange(false)">
<VButton @click="modal.close()">
{{ $t("core.common.buttons.cancel_and_shortcut") }}
</VButton>
</VSpace>

View File

@ -7,7 +7,7 @@ import {
VModal,
} from "@halo-dev/components";
import { ref, watch } from "vue";
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
import type { PolicyTemplate } from "@halo-dev/api-client";
import {
useFetchAttachmentPolicy,
useFetchAttachmentPolicyTemplate,
@ -16,17 +16,7 @@ import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
import { useLocalStorage } from "@vueuse/core";
const props = withDefaults(
defineProps<{
visible: boolean;
}>(),
{
visible: false,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
@ -34,11 +24,11 @@ const { groups } = useFetchAttachmentGroup();
const { policies, handleFetchPolicies } = useFetchAttachmentPolicy();
const { policyTemplates } = useFetchAttachmentPolicyTemplate();
const modal = ref();
const selectedGroupName = useLocalStorage("attachment-upload-group", "");
const selectedPolicyName = useLocalStorage("attachment-upload-policy", "");
const policyToCreate = ref<Policy>();
const uploadVisible = ref(false);
const policyEditingModal = ref(false);
const policyTemplateNameToCreate = ref();
watch(
() => groups.value,
@ -71,57 +61,24 @@ watch(
);
const handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {
policyToCreate.value = {
spec: {
displayName: "",
templateName: policyTemplate.metadata.name,
configMapName: "",
},
apiVersion: "storage.halo.run/v1alpha1",
kind: "Policy",
metadata: {
name: "",
generateName: "attachment-policy-",
},
};
policyTemplateNameToCreate.value = policyTemplate.metadata.name;
policyEditingModal.value = true;
};
const onEditingModalClose = async () => {
await handleFetchPolicies();
policyToCreate.value = policies.value?.[0];
};
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
selectedPolicyName.value = policies.value?.[0].metadata.name;
policyEditingModal.value = false;
}
};
watch(
() => props.visible,
(newValue) => {
if (newValue) {
uploadVisible.value = true;
} else {
const uploadVisibleTimer = setTimeout(() => {
uploadVisible.value = false;
clearTimeout(uploadVisibleTimer);
}, 200);
}
}
);
</script>
<template>
<VModal
ref="modal"
:body-class="['!p-0']"
:visible="visible"
:width="650"
:centered="false"
:title="$t('core.attachment.upload_modal.title')"
@update:visible="onVisibleChange"
@close="emit('close')"
>
<div class="w-full p-4">
<div class="mb-2">
@ -213,7 +170,6 @@ watch(
/>
</div>
<UppyUpload
v-if="uploadVisible"
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
:disabled="!selectedPolicyName"
:meta="{
@ -226,15 +182,14 @@ watch(
? ''
: $t('core.attachment.upload_modal.filters.policy.not_select')
"
:done-button-handler="() => onVisibleChange(false)"
:done-button-handler="() => modal.close()"
/>
</div>
</VModal>
<AttachmentPolicyEditingModal
v-if="visible"
v-model:visible="policyEditingModal"
:policy="policyToCreate"
v-if="policyEditingModal"
:template-name="policyTemplateNameToCreate"
@close="onEditingModalClose"
/>
</template>

View File

@ -1,18 +1,18 @@
<script lang="ts" setup>
import {
IconCheckboxFill,
IconArrowLeft,
IconArrowRight,
IconCheckboxCircle,
IconCheckboxFill,
IconEye,
IconUpload,
VButton,
VCard,
VEmpty,
VSpace,
VButton,
IconUpload,
VPagination,
IconEye,
IconCheckboxCircle,
VSpace,
} from "@halo-dev/components";
import { watchEffect, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
import { isImage } from "@/utils/image";
import { useAttachmentControl } from "../../composables/use-attachment";
import LazyImage from "@/components/image/LazyImage.vue";
@ -22,7 +22,6 @@ import AttachmentUploadModal from "../AttachmentUploadModal.vue";
import AttachmentDetailModal from "../AttachmentDetailModal.vue";
import AttachmentGroupList from "../AttachmentGroupList.vue";
import { matchMediaTypes } from "@/utils/media-type";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
@ -44,7 +43,7 @@ const emit = defineEmits<{
(event: "change-provider", providerId: string): void;
}>();
const selectedGroup = ref<Group>();
const selectedGroup = ref();
const page = ref(1);
const size = ref(60);
@ -61,7 +60,7 @@ const {
handleReset,
isChecked,
} = useAttachmentControl({
group: selectedGroup,
groupName: selectedGroup,
accepts: computed(() => {
return props.accepts;
}),
@ -97,13 +96,19 @@ const isDisabled = (attachment: Attachment) => {
return !isMatchMediaType;
};
function onUploadModalClose() {
handleFetchAttachments();
uploadVisible.value = false;
}
function onGroupSelect(group: Group) {
selectedGroup.value = group.metadata.name;
handleReset();
}
</script>
<template>
<AttachmentGroupList
v-model:selected-group="selectedGroup"
readonly
@select="handleReset"
/>
<AttachmentGroupList readonly @select="onGroupSelect" />
<div v-if="attachments?.length" class="mb-5">
<VButton @click="uploadVisible = true">
<template #icon>
@ -216,10 +221,7 @@ const isDisabled = (attachment: Attachment) => {
:size-options="[60, 120, 200]"
/>
</div>
<AttachmentUploadModal
v-model:visible="uploadVisible"
@close="handleFetchAttachments"
/>
<AttachmentUploadModal v-if="uploadVisible" @close="onUploadModalClose" />
<AttachmentDetailModal
v-model:visible="detailVisible"
:mount-to-body="true"

View File

@ -20,14 +20,15 @@ export function useFetchAttachmentGroup(): useFetchAttachmentGroupReturn {
sort: ["metadata.creationTimestamp,asc"],
}
);
return data.items;
},
refetchInterval(data) {
const deletingGroups = data?.filter(
const hasDeletingGroup = data?.some(
(group) => !!group.metadata.deletionTimestamp
);
return deletingGroups?.length ? 1000 : false;
return hasDeletingGroup ? 1000 : false;
},
});

View File

@ -24,10 +24,10 @@ export function useFetchAttachmentPolicy(): useFetchAttachmentPolicyReturn {
return data.items;
},
refetchInterval(data) {
const deletingPolicies = data?.filter(
const hasDeletingPolicy = data?.some(
(policy) => !!policy.metadata.deletionTimestamp
);
return deletingPolicies?.length ? 1000 : false;
return hasDeletingPolicy ? 1000 : false;
},
});

View File

@ -1,6 +1,5 @@
import type { Attachment, Group, Policy } from "@halo-dev/api-client";
import { computed, nextTick, type Ref } from "vue";
import { ref, watch } from "vue";
import type { Attachment } from "@halo-dev/api-client";
import { computed, nextTick, type Ref, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import { Dialog, Toast } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
@ -27,8 +26,8 @@ interface useAttachmentControlReturn {
}
export function useAttachmentControl(filterOptions: {
policy?: Ref<Policy | undefined>;
group?: Ref<Group | undefined>;
policyName?: Ref<string | undefined>;
groupName?: Ref<string | undefined>;
user?: Ref<string | undefined>;
accepts?: Ref<string[]>;
keyword?: Ref<string | undefined>;
@ -38,7 +37,7 @@ export function useAttachmentControl(filterOptions: {
}): useAttachmentControlReturn {
const { t } = useI18n();
const { user, policy, group, keyword, sort, page, size, accepts } =
const { user, policyName, groupName, keyword, sort, page, size, accepts } =
filterOptions;
const selectedAttachment = ref<Attachment>();
@ -52,9 +51,9 @@ export function useAttachmentControl(filterOptions: {
const { data, isLoading, isFetching, refetch } = useQuery<Attachment[]>({
queryKey: [
"attachments",
policy,
policyName,
keyword,
group,
groupName,
user,
accepts,
page,
@ -62,12 +61,12 @@ export function useAttachmentControl(filterOptions: {
sort,
],
queryFn: async () => {
const isUnGrouped = group?.value?.metadata.name === "ungrouped";
const isUnGrouped = groupName?.value === "ungrouped";
const fieldSelectorMap: Record<string, string | undefined> = {
"spec.policyName": policy?.value?.metadata.name,
"spec.policyName": policyName?.value,
"spec.ownerName": user?.value,
"spec.groupName": isUnGrouped ? undefined : group?.value?.metadata.name,
"spec.groupName": isUnGrouped ? undefined : groupName?.value,
};
const fieldSelector = Object.entries(fieldSelectorMap)
@ -95,10 +94,10 @@ export function useAttachmentControl(filterOptions: {
return data.items;
},
refetchInterval(data) {
const deletingAttachments = data?.filter(
const hasDeletingAttachment = data?.some(
(attachment) => !!attachment.metadata.deletionTimestamp
);
return deletingAttachments?.length ? 1000 : false;
return hasDeletingAttachment ? 1000 : false;
},
});