2023-07-11 07:15:10 +00:00
|
|
|
import type { Attachment, Group, Policy } from "@halo-dev/api-client";
|
2023-07-31 03:42:40 +00:00
|
|
|
import { computed, nextTick, type Ref } from "vue";
|
2022-09-04 17:06:11 +00:00
|
|
|
import { ref, watch } from "vue";
|
2022-09-22 08:46:32 +00:00
|
|
|
import { apiClient } from "@/utils/api-client";
|
2022-12-20 11:04:29 +00:00
|
|
|
import { Dialog, Toast } from "@halo-dev/components";
|
2023-02-23 09:08:12 +00:00
|
|
|
import { useQuery } from "@tanstack/vue-query";
|
2023-03-23 08:54:33 +00:00
|
|
|
import { useI18n } from "vue-i18n";
|
2023-04-26 07:32:12 +00:00
|
|
|
import { useClipboard } from "@vueuse/core";
|
|
|
|
import { matchMediaType } from "@/utils/media-type";
|
2022-09-04 17:06:11 +00:00
|
|
|
|
|
|
|
interface useAttachmentControlReturn {
|
2023-02-23 09:08:12 +00:00
|
|
|
attachments: Ref<Attachment[] | undefined>;
|
|
|
|
isLoading: Ref<boolean>;
|
|
|
|
isFetching: Ref<boolean>;
|
2022-09-04 17:06:11 +00:00
|
|
|
selectedAttachment: Ref<Attachment | undefined>;
|
|
|
|
selectedAttachments: Ref<Set<Attachment>>;
|
|
|
|
checkedAll: Ref<boolean>;
|
2023-02-23 09:08:12 +00:00
|
|
|
total: Ref<number>;
|
|
|
|
handleFetchAttachments: () => void;
|
2022-09-04 17:06:11 +00:00
|
|
|
handleSelectPrevious: () => void;
|
|
|
|
handleSelectNext: () => void;
|
|
|
|
handleDeleteInBatch: () => void;
|
|
|
|
handleCheckAll: (checkAll: boolean) => void;
|
|
|
|
handleSelect: (attachment: Attachment | undefined) => void;
|
|
|
|
isChecked: (attachment: Attachment) => boolean;
|
|
|
|
handleReset: () => void;
|
|
|
|
}
|
|
|
|
|
2023-02-23 09:08:12 +00:00
|
|
|
export function useAttachmentControl(filterOptions: {
|
2022-09-04 17:06:11 +00:00
|
|
|
policy?: Ref<Policy | undefined>;
|
|
|
|
group?: Ref<Group | undefined>;
|
2023-07-11 07:15:10 +00:00
|
|
|
user?: Ref<string | undefined>;
|
2022-09-04 17:06:11 +00:00
|
|
|
keyword?: Ref<string | undefined>;
|
2022-11-17 02:50:22 +00:00
|
|
|
sort?: Ref<string | undefined>;
|
2023-02-23 09:08:12 +00:00
|
|
|
page: Ref<number>;
|
|
|
|
size: Ref<number>;
|
2022-09-04 17:06:11 +00:00
|
|
|
}): useAttachmentControlReturn {
|
2023-03-23 08:54:33 +00:00
|
|
|
const { t } = useI18n();
|
|
|
|
|
2023-02-23 09:08:12 +00:00
|
|
|
const { user, policy, group, keyword, sort, page, size } = filterOptions;
|
2022-09-04 17:06:11 +00:00
|
|
|
|
|
|
|
const selectedAttachment = ref<Attachment>();
|
|
|
|
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
|
|
|
|
const checkedAll = ref(false);
|
2022-10-24 14:10:10 +00:00
|
|
|
|
2023-02-23 09:08:12 +00:00
|
|
|
const total = ref(0);
|
|
|
|
const hasPrevious = ref(false);
|
|
|
|
const hasNext = ref(false);
|
2022-11-23 04:59:28 +00:00
|
|
|
|
2023-02-23 09:08:12 +00:00
|
|
|
const { data, isLoading, isFetching, refetch } = useQuery<Attachment[]>({
|
|
|
|
queryKey: ["attachments", policy, keyword, group, user, page, size, sort],
|
|
|
|
queryFn: async () => {
|
2024-02-20 02:58:09 +00:00
|
|
|
const isUnGrouped = group?.value?.metadata.name === "ungrouped";
|
|
|
|
|
|
|
|
const fieldSelectorMap: Record<string, string | undefined> = {
|
|
|
|
"spec.policyName": policy?.value?.metadata.name,
|
|
|
|
"spec.ownerName": user?.value,
|
|
|
|
"spec.groupName": isUnGrouped ? undefined : group?.value?.metadata.name,
|
|
|
|
};
|
|
|
|
|
|
|
|
const fieldSelector = Object.entries(fieldSelectorMap)
|
|
|
|
.map(([key, value]) => {
|
|
|
|
if (value) {
|
|
|
|
return `${key}=${value}`;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter(Boolean) as string[];
|
|
|
|
|
2022-10-17 14:53:39 +00:00
|
|
|
const { data } = await apiClient.attachment.searchAttachments({
|
2024-02-20 02:58:09 +00:00
|
|
|
fieldSelector,
|
|
|
|
page: page.value,
|
|
|
|
size: size.value,
|
|
|
|
ungrouped: isUnGrouped,
|
|
|
|
keyword: keyword?.value,
|
2022-11-17 02:50:22 +00:00
|
|
|
sort: [sort?.value as string].filter(Boolean),
|
2022-10-17 14:53:39 +00:00
|
|
|
});
|
2022-10-24 14:10:10 +00:00
|
|
|
|
2023-02-23 09:08:12 +00:00
|
|
|
total.value = data.total;
|
|
|
|
hasPrevious.value = data.hasPrevious;
|
|
|
|
hasNext.value = data.hasNext;
|
|
|
|
|
|
|
|
return data.items;
|
|
|
|
},
|
|
|
|
refetchInterval(data) {
|
|
|
|
const deletingAttachments = data?.filter(
|
2022-10-24 14:10:10 +00:00
|
|
|
(attachment) => !!attachment.metadata.deletionTimestamp
|
|
|
|
);
|
2023-07-19 03:44:12 +00:00
|
|
|
return deletingAttachments?.length ? 1000 : false;
|
2023-02-23 09:08:12 +00:00
|
|
|
},
|
2022-10-24 14:10:10 +00:00
|
|
|
});
|
|
|
|
|
2022-09-04 17:06:11 +00:00
|
|
|
const handleSelectPrevious = async () => {
|
2023-02-23 09:08:12 +00:00
|
|
|
if (!data.value) return;
|
|
|
|
|
|
|
|
const index = data.value?.findIndex(
|
2022-09-04 17:06:11 +00:00
|
|
|
(attachment) =>
|
|
|
|
attachment.metadata.name === selectedAttachment.value?.metadata.name
|
|
|
|
);
|
2023-02-23 09:08:12 +00:00
|
|
|
|
|
|
|
if (index === undefined) return;
|
|
|
|
|
2022-09-04 17:06:11 +00:00
|
|
|
if (index > 0) {
|
2023-02-23 09:08:12 +00:00
|
|
|
selectedAttachment.value = data.value[index - 1];
|
2022-09-04 17:06:11 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-02-23 09:08:12 +00:00
|
|
|
|
2023-07-29 09:12:18 +00:00
|
|
|
if (index === 0 && hasPrevious.value) {
|
2023-02-23 09:08:12 +00:00
|
|
|
page.value--;
|
2023-07-31 03:42:40 +00:00
|
|
|
await nextTick();
|
2023-02-23 09:08:12 +00:00
|
|
|
await refetch();
|
|
|
|
selectedAttachment.value = data.value[data.value.length - 1];
|
2022-09-04 17:06:11 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSelectNext = async () => {
|
2023-02-23 09:08:12 +00:00
|
|
|
if (!data.value) return;
|
|
|
|
|
|
|
|
const index = data.value?.findIndex(
|
2022-09-04 17:06:11 +00:00
|
|
|
(attachment) =>
|
|
|
|
attachment.metadata.name === selectedAttachment.value?.metadata.name
|
|
|
|
);
|
2023-02-23 09:08:12 +00:00
|
|
|
|
|
|
|
if (index === undefined) return;
|
|
|
|
|
|
|
|
if (index < data.value?.length - 1) {
|
|
|
|
selectedAttachment.value = data.value[index + 1];
|
2022-09-04 17:06:11 +00:00
|
|
|
return;
|
|
|
|
}
|
2023-02-23 09:08:12 +00:00
|
|
|
|
2023-07-29 09:12:18 +00:00
|
|
|
if (index === data.value.length - 1 && hasNext.value) {
|
2023-02-23 09:08:12 +00:00
|
|
|
page.value++;
|
2023-07-31 03:42:40 +00:00
|
|
|
await nextTick();
|
2023-02-23 09:08:12 +00:00
|
|
|
await refetch();
|
|
|
|
selectedAttachment.value = data.value[0];
|
2022-09-04 17:06:11 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleDeleteInBatch = () => {
|
2022-10-18 01:58:09 +00:00
|
|
|
Dialog.warning({
|
2023-03-23 08:54:33 +00:00
|
|
|
title: t("core.attachment.operations.delete_in_batch.title"),
|
|
|
|
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
2022-09-04 17:06:11 +00:00
|
|
|
confirmType: "danger",
|
2023-03-23 08:54:33 +00:00
|
|
|
confirmText: t("core.common.buttons.confirm"),
|
|
|
|
cancelText: t("core.common.buttons.cancel"),
|
2022-09-04 17:06:11 +00:00
|
|
|
onConfirm: async () => {
|
|
|
|
try {
|
|
|
|
const promises = Array.from(selectedAttachments.value).map(
|
|
|
|
(attachment) => {
|
|
|
|
return apiClient.extension.storage.attachment.deletestorageHaloRunV1alpha1Attachment(
|
refactor: method parameters of api client (halo-dev/console#605)
<!-- Thanks for sending a pull request! Here are some tips for you:
1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。
1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>.
2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.
3. 请确保你已经添加并运行了适当的测试。
3. Ensure you have added or ran the appropriate tests for your PR.
-->
#### What type of PR is this?
/kind improvement
/milestone 2.0
<!--
添加其中一个类别:
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind optimization
适当添加其中一个或多个类别(可选):
Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
#### What this PR does / why we need it:
修改 api-client 的请求参数结构,改为所有参数由一个对象包裹,而不是将各个参数作为方法的参数,防止因为后端参数结构发生改变,或者生成 api-client 时参数顺序发生改变导致请求异常。如:
```diff
await apiClient.extension.storage.group.updatestorageHaloRunV1alpha1Group(
- formState.value.metadata.name,
- formState.value
+ {
+ name: formState.value.metadata.name,
+ group: formState.value,
+ }
);
```
#### Which issue(s) this PR fixes:
<!--
PR 合并时自动关闭 issue。
Automatically closes linked issue when PR is merged.
用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)`
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
None
#### Screenshots:
<!--
如果此 PR 有 UI 的改动,最好截图说明这个 PR 的改动。
If there are UI changes to this PR, it is best to take a screenshot to illustrate the changes to this PR.
eg.
Before:

After:

-->
None
#### Special notes for your reviewer:
/cc @halo-dev/sig-halo-admin
#### Does this PR introduce a user-facing change?
<!--
如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。
否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change),
Release Note 需要以 `action required` 开头。
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
None
```
2022-09-06 02:26:11 +00:00
|
|
|
{
|
|
|
|
name: attachment.metadata.name,
|
|
|
|
}
|
2022-09-04 17:06:11 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
|
|
selectedAttachments.value.clear();
|
2022-12-20 11:04:29 +00:00
|
|
|
|
2023-03-23 08:54:33 +00:00
|
|
|
Toast.success(t("core.common.toast.delete_success"));
|
2022-09-04 17:06:11 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error("Failed to delete attachments", e);
|
|
|
|
} finally {
|
2023-02-23 09:08:12 +00:00
|
|
|
await refetch();
|
2022-09-04 17:06:11 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleCheckAll = (checkAll: boolean) => {
|
|
|
|
if (checkAll) {
|
2023-02-23 09:08:12 +00:00
|
|
|
data.value?.forEach((attachment) => {
|
2022-09-04 17:06:11 +00:00
|
|
|
selectedAttachments.value.add(attachment);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
selectedAttachments.value.clear();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSelect = async (attachment: Attachment | undefined) => {
|
|
|
|
if (!attachment) return;
|
|
|
|
if (selectedAttachments.value.has(attachment)) {
|
|
|
|
selectedAttachments.value.delete(attachment);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
selectedAttachments.value.add(attachment);
|
|
|
|
};
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => selectedAttachments.value.size,
|
|
|
|
(newValue) => {
|
2023-02-23 09:08:12 +00:00
|
|
|
checkedAll.value = newValue === data.value?.length;
|
2022-09-04 17:06:11 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const isChecked = (attachment: Attachment) => {
|
|
|
|
return (
|
|
|
|
attachment.metadata.name === selectedAttachment.value?.metadata.name ||
|
|
|
|
Array.from(selectedAttachments.value)
|
|
|
|
.map((item) => item.metadata.name)
|
|
|
|
.includes(attachment.metadata.name)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleReset = () => {
|
2023-02-23 09:08:12 +00:00
|
|
|
page.value = 1;
|
2022-09-04 17:06:11 +00:00
|
|
|
selectedAttachment.value = undefined;
|
|
|
|
selectedAttachments.value.clear();
|
|
|
|
checkedAll.value = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
2023-02-23 09:08:12 +00:00
|
|
|
attachments: data,
|
|
|
|
isLoading,
|
|
|
|
isFetching,
|
2022-09-04 17:06:11 +00:00
|
|
|
selectedAttachment,
|
|
|
|
selectedAttachments,
|
|
|
|
checkedAll,
|
2023-02-23 09:08:12 +00:00
|
|
|
total,
|
|
|
|
handleFetchAttachments: refetch,
|
2022-09-04 17:06:11 +00:00
|
|
|
handleSelectPrevious,
|
|
|
|
handleSelectNext,
|
|
|
|
handleDeleteInBatch,
|
|
|
|
handleCheckAll,
|
|
|
|
handleSelect,
|
|
|
|
isChecked,
|
|
|
|
handleReset,
|
|
|
|
};
|
|
|
|
}
|
2022-09-08 08:49:42 +00:00
|
|
|
|
2023-04-26 07:32:12 +00:00
|
|
|
export function useAttachmentPermalinkCopy(
|
|
|
|
attachment: Ref<Attachment | undefined>
|
|
|
|
) {
|
2023-06-26 13:28:08 +00:00
|
|
|
const { copy } = useClipboard({ legacy: true });
|
2023-04-26 07:32:12 +00:00
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
|
|
const mediaType = computed(() => {
|
|
|
|
return attachment.value?.spec.mediaType;
|
|
|
|
});
|
|
|
|
|
|
|
|
const isImage = computed(() => {
|
|
|
|
return mediaType.value && matchMediaType(mediaType.value, "image/*");
|
|
|
|
});
|
|
|
|
|
|
|
|
const isVideo = computed(() => {
|
|
|
|
return mediaType.value && matchMediaType(mediaType.value, "video/*");
|
|
|
|
});
|
|
|
|
|
|
|
|
const isAudio = computed(() => {
|
|
|
|
return mediaType.value && matchMediaType(mediaType.value, "audio/*");
|
|
|
|
});
|
|
|
|
|
|
|
|
const htmlText = computed(() => {
|
|
|
|
const { permalink } = attachment.value?.status || {};
|
|
|
|
const { displayName } = attachment.value?.spec || {};
|
|
|
|
|
|
|
|
if (isImage.value) {
|
|
|
|
return `<img src="${permalink}" alt="${displayName}" />`;
|
|
|
|
} else if (isVideo.value) {
|
|
|
|
return `<video src="${permalink}"></video>`;
|
|
|
|
} else if (isAudio.value) {
|
|
|
|
return `<audio src="${permalink}"></audio>`;
|
|
|
|
}
|
|
|
|
return `<a href="${permalink}">${displayName}</a>`;
|
|
|
|
});
|
|
|
|
|
|
|
|
const markdownText = computed(() => {
|
|
|
|
const { permalink } = attachment.value?.status || {};
|
|
|
|
const { displayName } = attachment.value?.spec || {};
|
|
|
|
if (isImage.value) {
|
|
|
|
return ``;
|
|
|
|
}
|
|
|
|
return `[${displayName}](${permalink})`;
|
|
|
|
});
|
|
|
|
|
|
|
|
const handleCopy = (format: "markdown" | "html" | "url") => {
|
|
|
|
const { permalink } = attachment.value?.status || {};
|
|
|
|
|
|
|
|
if (!permalink) return;
|
|
|
|
|
|
|
|
if (format === "url") {
|
|
|
|
copy(permalink);
|
|
|
|
} else if (format === "markdown") {
|
|
|
|
copy(markdownText.value);
|
|
|
|
} else if (format === "html") {
|
|
|
|
copy(htmlText.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
Toast.success(t("core.common.toast.copy_success"));
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
htmlText,
|
|
|
|
markdownText,
|
|
|
|
handleCopy,
|
|
|
|
};
|
|
|
|
}
|