Skip to content

Commit

Permalink
feat: support filtering attachments in the library by file media type (
Browse files Browse the repository at this point in the history
…#5893)

#### What type of PR is this?

/kind feature
/area core
/area ui

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

为 `/apis/api.console.halo.run/v1alpha1/attachments` 接口增加了 `accepts` 可选参数,用于根据附件的 `MediaType` 进行筛选。

为附件库增加通过文件的 MediaType 类型进行筛选的筛选项。

同时支持使用了 `CoreSelectorProvider` 组件的文件选择框的筛选。现在只会显示 `accepts` 所支持的文件。

#### How to test it?

测试 ui 端文件选择框的类型筛选是否正确有效。
测试使用了 `CoreSelectorProvider` 组件的 `accepts` 是否有效。

#### Which issue(s) this PR fixes:

Fixes #5054

#### Does this PR introduce a user-facing change?
```release-note
附件库支持按文件类型进行过滤
```
  • Loading branch information
LIlGG authored May 16, 2024
1 parent 983e70d commit e5bc699
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 81 deletions.
11 changes: 11 additions & 0 deletions api-docs/openapi/v3_0/aggregated.json
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,17 @@
"schema": {
"type": "string"
}
},
{
"description": "Acceptable media types.",
"in": "query",
"name": "accepts",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
Expand All @@ -15,11 +16,13 @@
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.index.query.QueryFactory.not;
import static run.halo.app.extension.index.query.QueryFactory.startsWith;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -32,6 +35,7 @@
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyExtractors;
Expand All @@ -49,6 +53,7 @@
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil;
Expand Down Expand Up @@ -142,6 +147,14 @@ public interface ISearchRequest extends IListRequest {
+ " parameter.")
Optional<Boolean> getUngrouped();

@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "accepts",
description = "Acceptable media types."),
schema = @Schema(description = "like image/*, video/mp4, text/*",
implementation = String.class,
example = "image/*"))
List<String> getAccepts();

@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
Expand All @@ -168,7 +181,21 @@ public static void buildParameters(Builder builder) {
.name("keyword")
.required(false)
.description("Keyword for searching.")
.implementation(String.class));
.implementation(String.class))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("accepts")
.required(false)
.description("Acceptable media types.")
.array(
arraySchemaBuilder()
.uniqueItems(true)
.schema(schemaBuilder()
.implementation(String.class)
.example("image/*"))
)
.implementationArray(String.class)
);
}
}

Expand All @@ -193,6 +220,11 @@ public Optional<Boolean> getUngrouped() {
.map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));
}

@Override
public List<String> getAccepts() {
return queryParams.getOrDefault("accepts", Collections.emptyList());
}

@Override
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
Expand Down Expand Up @@ -220,9 +252,27 @@ public ListOptions toListOptions(List<String> hiddenGroups) {
fieldQuery = and(fieldQuery, not(in("spec.groupName", hiddenGroups)));
}

if (hasAccepts()) {
var acceptFieldQueryOptional = getAccepts().stream()
.filter(StringUtils::hasText)
.map((accept -> accept.replace("/*", "/").toLowerCase()))
.distinct()
.map(accept -> startsWith("spec.mediaType", accept))
.reduce(QueryFactory::or);
if (acceptFieldQueryOptional.isPresent()) {
fieldQuery = and(fieldQuery, acceptFieldQueryOptional.get());
}
}

listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
return listOptions;
}

private boolean hasAccepts() {
return !CollectionUtils.isEmpty(getAccepts())
&& !getAccepts().contains("*")
&& !getAccepts().contains("*/*");
}
}

@Schema(types = "object")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,13 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event
return StringUtils.isBlank(group) ? null : group;
}))
);
indexSpecs.add(new IndexSpec()
.setName("spec.mediaType")
.setIndexFunc(simpleAttribute(Attachment.class, attachment -> {
var mediaType = attachment.getSpec().getMediaType();
return StringUtils.isBlank(mediaType) ? null : mediaType;
}))
);
indexSpecs.add(new IndexSpec()
.setName("spec.ownerName")
.setIndexFunc(simpleAttribute(Attachment.class,
Expand Down
41 changes: 40 additions & 1 deletion ui/console-src/modules/contents/attachments/AttachmentList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ const size = useRouteQuery<number>("size", 60, {
const selectedPolicy = useRouteQuery<string | undefined>("policy");
const selectedUser = useRouteQuery<string | undefined>("user");
const selectedSort = useRouteQuery<string | undefined>("sort");
const selectedAccepts = useRouteQuery<string | undefined>("accepts");
watch(
() => [
selectedPolicy.value,
selectedUser.value,
selectedSort.value,
selectedAccepts.value,
keyword.value,
],
() => {
Expand All @@ -77,13 +79,19 @@ watch(
);
const hasFilters = computed(() => {
return selectedPolicy.value || selectedUser.value || selectedSort.value;
return (
selectedPolicy.value ||
selectedUser.value ||
selectedSort.value ||
selectedAccepts.value
);
});
function handleClearFilters() {
selectedPolicy.value = undefined;
selectedUser.value = undefined;
selectedSort.value = undefined;
selectedAccepts.value = undefined;
}
const {
Expand All @@ -110,6 +118,12 @@ const {
);
}),
user: selectedUser,
accepts: computed(() => {
if (!selectedAccepts.value) {
return [];
}
return selectedAccepts.value.split(",");
}),
keyword: keyword,
sort: selectedSort,
page: page,
Expand Down Expand Up @@ -348,6 +362,31 @@ onMounted(() => {
}) || []),
]"
/>
<FilterDropdown
v-model="selectedAccepts"
:label="$t('core.attachment.filters.accept.label')"
:items="[
{
label: t('core.common.filters.item_labels.all'),
},
{
label: t('core.attachment.filters.accept.items.image'),
value: 'image/*',
},
{
label: t('core.attachment.filters.accept.items.audio'),
value: 'audio/*',
},
{
label: t('core.attachment.filters.accept.items.video'),
value: 'video/*',
},
{
label: t('core.attachment.filters.accept.items.file'),
value: 'text/*,application/*',
},
]"
/>
<HasPermission :permissions="['system:users:view']">
<UserFilterDropdown
v-model="selectedUser"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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<{
Expand Down Expand Up @@ -59,7 +60,14 @@ const {
handleSelectNext,
handleReset,
isChecked,
} = useAttachmentControl({ group: selectedGroup, page, size });
} = useAttachmentControl({
group: selectedGroup,
accepts: computed(() => {
return props.accepts;
}),
page,
size,
});
const uploadVisible = ref(false);
const detailVisible = ref(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export function useAttachmentControl(filterOptions: {
policy?: Ref<Policy | undefined>;
group?: Ref<Group | undefined>;
user?: Ref<string | undefined>;
accepts?: Ref<string[]>;
keyword?: Ref<string | undefined>;
sort?: Ref<string | undefined>;
page: Ref<number>;
size: Ref<number>;
}): useAttachmentControlReturn {
const { t } = useI18n();

const { user, policy, group, keyword, sort, page, size } = filterOptions;
const { user, policy, group, keyword, sort, page, size, accepts } =
filterOptions;

const selectedAttachment = ref<Attachment>();
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
Expand All @@ -48,7 +50,17 @@ export function useAttachmentControl(filterOptions: {
const hasNext = ref(false);

const { data, isLoading, isFetching, refetch } = useQuery<Attachment[]>({
queryKey: ["attachments", policy, keyword, group, user, page, size, sort],
queryKey: [
"attachments",
policy,
keyword,
group,
user,
accepts,
page,
size,
sort,
],
queryFn: async () => {
const isUnGrouped = group?.value?.metadata.name === "ungrouped";

Expand All @@ -71,6 +83,7 @@ export function useAttachmentControl(filterOptions: {
page: page.value,
size: size.value,
ungrouped: isUnGrouped,
accepts: accepts?.value,
keyword: keyword?.value,
sort: [sort?.value as string].filter(Boolean),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export const ApiConsoleHaloRunV1alpha1AttachmentApiAxiosParamCreator = function
* @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.
* @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.
* @param {string} [keyword] Keyword for searching.
* @param {Array<string>} [accepts] Acceptable media types.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchAttachments: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
searchAttachments: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/apis/api.console.halo.run/v1alpha1/attachments`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
Expand Down Expand Up @@ -92,6 +93,10 @@ export const ApiConsoleHaloRunV1alpha1AttachmentApiAxiosParamCreator = function
localVarQueryParameter['keyword'] = keyword;
}

if (accepts) {
localVarQueryParameter['accepts'] = accepts;
}



setSearchParams(localVarUrlObj, localVarQueryParameter);
Expand Down Expand Up @@ -182,11 +187,12 @@ export const ApiConsoleHaloRunV1alpha1AttachmentApiFp = function(configuration?:
* @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.
* @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.
* @param {string} [keyword] Keyword for searching.
* @param {Array<string>} [accepts] Acceptable media types.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async searchAttachments(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttachmentList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchAttachments(page, size, labelSelector, fieldSelector, sort, ungrouped, keyword, options);
async searchAttachments(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttachmentList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchAttachments(page, size, labelSelector, fieldSelector, sort, ungrouped, keyword, accepts, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1AttachmentApi.searchAttachments']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
Expand Down Expand Up @@ -222,7 +228,7 @@ export const ApiConsoleHaloRunV1alpha1AttachmentApiFactory = function (configura
* @throws {RequiredError}
*/
searchAttachments(requestParameters: ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachmentsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AttachmentList> {
return localVarFp.searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, options).then((request) => request(axios, basePath));
return localVarFp.searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(axios, basePath));
},
/**
*
Expand Down Expand Up @@ -290,6 +296,13 @@ export interface ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachmentsRequest
* @memberof ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachments
*/
readonly keyword?: string

/**
* Acceptable media types.
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachments
*/
readonly accepts?: Array<string>
}

/**
Expand Down Expand Up @@ -335,7 +348,7 @@ export class ApiConsoleHaloRunV1alpha1AttachmentApi extends BaseAPI {
* @memberof ApiConsoleHaloRunV1alpha1AttachmentApi
*/
public searchAttachments(requestParameters: ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachmentsRequest = {}, options?: RawAxiosRequestConfig) {
return ApiConsoleHaloRunV1alpha1AttachmentApiFp(this.configuration).searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, options).then((request) => request(this.axios, this.basePath));
return ApiConsoleHaloRunV1alpha1AttachmentApiFp(this.configuration).searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(this.axios, this.basePath));
}

/**
Expand Down
7 changes: 7 additions & 0 deletions ui/src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,13 @@ core:
items:
grid: Grid Mode
list: List Mode
accept:
label: Type
items:
image: Image
audio: Audio
video: Video
file: File
detail_modal:
title: "Attachment: {display_name}"
fields:
Expand Down
Loading

0 comments on commit e5bc699

Please sign in to comment.