feat: support filtering attachments in the library by file media type (#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
附件库支持按文件类型进行过滤
```
pull/5928/head
Takagi 2024-05-16 10:32:35 +08:00 committed by GitHub
parent 983e70d50d
commit e5bc699fb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 372 additions and 81 deletions

View File

@ -1869,6 +1869,17 @@
"schema": {
"type": "string"
}
},
{
"description": "Acceptable media types.",
"in": "query",
"name": "accepts",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {

View File

@ -2,6 +2,7 @@ package run.halo.app.core.extension.attachment.endpoint;
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;
@ -15,11 +16,13 @@ import static run.halo.app.extension.index.query.QueryFactory.contains;
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;
@ -32,6 +35,7 @@ import org.springframework.http.codec.multipart.FilePart;
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;
@ -49,6 +53,7 @@ import run.halo.app.core.extension.service.AttachmentService;
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;
@ -142,6 +147,14 @@ public class AttachmentEndpoint implements CustomEndpoint {
+ " 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: "
@ -168,7 +181,21 @@ public class AttachmentEndpoint implements CustomEndpoint {
.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)
);
}
}
@ -193,6 +220,11 @@ public class AttachmentEndpoint implements CustomEndpoint {
.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);
@ -220,9 +252,27 @@ public class AttachmentEndpoint implements CustomEndpoint {
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")

View File

@ -390,6 +390,13 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
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,

View File

@ -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,
],
() => {
@ -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 {
@ -110,6 +118,12 @@ const {
);
}),
user: selectedUser,
accepts: computed(() => {
if (!selectedAccepts.value) {
return [];
}
return selectedAccepts.value.split(",");
}),
keyword: keyword,
sort: selectedSort,
page: page,
@ -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"

View File

@ -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<{
@ -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);

View File

@ -30,6 +30,7 @@ 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>;
@ -37,7 +38,8 @@ export function useAttachmentControl(filterOptions: {
}): 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>());
@ -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";
@ -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),
});

View File

@ -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);
@ -92,6 +93,10 @@ export const ApiConsoleHaloRunV1alpha1AttachmentApiAxiosParamCreator = function
localVarQueryParameter['keyword'] = keyword;
}
if (accepts) {
localVarQueryParameter['accepts'] = accepts;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -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);
@ -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));
},
/**
*
@ -290,6 +296,13 @@ export interface ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachmentsRequest
* @memberof ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachments
*/
readonly keyword?: string
/**
* Acceptable media types.
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1AttachmentApiSearchAttachments
*/
readonly accepts?: Array<string>
}
/**
@ -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));
}
/**

View File

@ -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:

View File

@ -9,7 +9,9 @@ core:
operations:
submit:
toast_success: Inicio de sesión exitoso
toast_failed: Error en el inicio de sesión, nombre de usuario o contraseña incorrectos
toast_failed: >-
Error en el inicio de sesión, nombre de usuario o contraseña
incorrectos
toast_csrf: Token CSRF no válido, por favor inténtalo de nuevo
signup:
label: No tienes una cuenta
@ -40,7 +42,9 @@ core:
title: Vinculación de cuentas
common:
toast:
mounted: El método de inicio de sesión actual no está vinculado a una cuenta. Por favor, vincula o registra una nueva cuenta primero.
mounted: >-
El método de inicio de sesión actual no está vinculado a una cuenta.
Por favor, vincula o registra una nueva cuenta primero.
operations:
login_and_bind:
button: Iniciar sesión y vincular
@ -48,7 +52,9 @@ core:
button: Registrarse y vincular
bind:
toast_success: Vinculación exitosa
toast_failed: Vinculación fallida, no se encontró ningún método de inicio de sesión habilitado.
toast_failed: >-
Vinculación fallida, no se encontró ningún método de inicio de sesión
habilitado.
sidebar:
search:
placeholder: Buscar
@ -124,7 +130,9 @@ core:
refresh_search_engine:
title: Actualizar Motor de Búsqueda
dialog_title: ¿Deseas actualizar el índice del motor de búsqueda?
dialog_content: Esta operación recreará los índices del motor de búsqueda para todas las publicaciones publicadas.
dialog_content: >-
Esta operación recreará los índices del motor de búsqueda para
todas las publicaciones publicadas.
success_message: Índice del motor de búsqueda actualizado exitosamente.
evict_page_cache:
title: Actualizar Caché de Página
@ -149,10 +157,14 @@ core:
operations:
delete:
title: ¿Estás seguro de que deseas eliminar esta publicación?
description: Esta operación moverá la publicación a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.
description: >-
Esta operación moverá la publicación a la papelera de reciclaje, y
podrá ser restaurada desde la papelera de reciclaje posteriormente.
delete_in_batch:
title: ¿Estás seguro de que deseas eliminar las publicaciones seleccionadas?
description: Esta operación moverá las publicaciones a la papelera de reciclaje, y podrán ser restauradas desde la papelera de reciclaje posteriormente.
description: >-
Esta operación moverá las publicaciones a la papelera de reciclaje, y
podrán ser restauradas desde la papelera de reciclaje posteriormente.
filters:
status:
items:
@ -198,7 +210,9 @@ core:
label: Título
slug:
label: Slug
help: Usualmente usado para generar el enlace permanente a las publicaciones
help: >-
Usualmente usado para generar el enlace permanente a las
publicaciones
refresh_message: Regenerar slug basado en el título.
categories:
label: Categorías
@ -230,14 +244,20 @@ core:
title: ¿Estás seguro de que deseas eliminar permanentemente esta publicación?
description: Después de la eliminación, no será posible recuperarla.
delete_in_batch:
title: ¿Estás seguro de que deseas eliminar permanentemente las publicaciones seleccionadas?
title: >-
¿Estás seguro de que deseas eliminar permanentemente las publicaciones
seleccionadas?
description: Después de la eliminación, no será posible recuperarlas.
recovery:
title: ¿Quieres restaurar esta publicación?
description: Esta operación restaurará la publicación a su estado antes de la eliminación.
description: >-
Esta operación restaurará la publicación a su estado antes de la
eliminación.
recovery_in_batch:
title: ¿Estás seguro de que deseas restaurar las publicaciones seleccionadas?
description: Esta operación restaurará las publicaciones a su estado antes de la eliminación.
description: >-
Esta operación restaurará las publicaciones a su estado antes de la
eliminación.
post_editor:
title: Edición de publicación
untitled: Publicación sin título
@ -251,7 +271,9 @@ core:
operations:
delete:
title: ¿Estás seguro de que deseas eliminar esta etiqueta?
description: Después de eliminar esta etiqueta, se eliminará la asociación con el artículo correspondiente. Esta operación no se puede deshacer.
description: >-
Después de eliminar esta etiqueta, se eliminará la asociación con el
artículo correspondiente. Esta operación no se puede deshacer.
editing_modal:
titles:
update: Actualizar etiqueta de publicación
@ -264,7 +286,9 @@ core:
label: Nombre para mostrar
slug:
label: Slug
help: Usualmente utilizado para generar el enlace permanente de las etiquetas
help: >-
Usualmente utilizado para generar el enlace permanente de las
etiquetas
refresh_message: Regenerar slug basado en el nombre para mostrar.
color:
label: Color
@ -282,7 +306,9 @@ core:
operations:
delete:
title: ¿Estás seguro de que deseas eliminar esta categoría?
description: Después de eliminar esta categoría, se eliminará la asociación con los artículos correspondientes. Esta operación no se puede deshacer.
description: >-
Después de eliminar esta categoría, se eliminará la asociación con los
artículos correspondientes. Esta operación no se puede deshacer.
add_sub_category:
button: Agregar subcategoría
editing_modal:
@ -299,7 +325,9 @@ core:
label: Nombre para mostrar
slug:
label: Slug
help: Usualmente utilizado para generar el enlace permanente de las categorías
help: >-
Usualmente utilizado para generar el enlace permanente de las
categorías
refresh_message: Regenerar slug basado en el nombre para mostrar.
template:
label: Plantilla personalizada
@ -319,10 +347,14 @@ core:
operations:
delete:
title: ¿Estás seguro de que deseas eliminar esta página?
description: Esta operación moverá la página a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.
description: >-
Esta operación moverá la página a la papelera de reciclaje, y podrá
ser restaurada desde la papelera de reciclaje posteriormente.
delete_in_batch:
title: ¿Estás seguro de que deseas eliminar las páginas seleccionadas?
description: Esta operación moverá las páginas a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.
description: >-
Esta operación moverá las páginas a la papelera de reciclaje, y podrá
ser restaurada desde la papelera de reciclaje posteriormente.
filters:
status:
items:
@ -358,7 +390,9 @@ core:
label: Título
slug:
label: Slug
help: Usualmente utilizado para generar el enlace permanente de las páginas
help: >-
Usualmente utilizado para generar el enlace permanente de las
páginas
refresh_message: Regenerar slug basado en el título.
auto_generate_excerpt:
label: Generar Extracto Automáticamente
@ -386,14 +420,20 @@ core:
title: ¿Estás seguro de que deseas eliminar permanentemente esta página?
description: Después de la eliminación, no será posible recuperarla.
delete_in_batch:
title: ¿Estás seguro de que deseas eliminar permanentemente las páginas seleccionadas?
title: >-
¿Estás seguro de que deseas eliminar permanentemente las páginas
seleccionadas?
description: Después de la eliminación, no será posible recuperarlas.
recovery:
title: ¿Quieres restaurar esta página?
description: Esta operación restaurará la página a su estado antes de la eliminación.
description: >-
Esta operación restaurará la página a su estado antes de la
eliminación.
recovery_in_batch:
title: ¿Estás seguro de que deseas restaurar las páginas seleccionadas?
description: Esta operación restaurará las páginas a su estado antes de la eliminación.
description: >-
Esta operación restaurará las páginas a su estado antes de la
eliminación.
page_editor:
title: Edición de Página
untitled: Página Sin Título
@ -409,16 +449,24 @@ core:
operations:
delete_comment:
title: ¿Estás seguro de que deseas eliminar este comentario?
description: Todas las respuestas bajo los comentarios se eliminarán al mismo tiempo, y esta operación no se puede deshacer.
description: >-
Todas las respuestas bajo los comentarios se eliminarán al mismo
tiempo, y esta operación no se puede deshacer.
delete_comment_in_batch:
title: ¿Estás seguro de que deseas eliminar los comentarios seleccionados?
description: Todas las respuestas bajo los comentarios se eliminarán al mismo tiempo, y esta operación no se puede deshacer.
description: >-
Todas las respuestas bajo los comentarios se eliminarán al mismo
tiempo, y esta operación no se puede deshacer.
approve_comment_in_batch:
button: Aprobar
title: ¿Estás seguro de que deseas aprobar los comentarios seleccionados para su revisión?
title: >-
¿Estás seguro de que deseas aprobar los comentarios seleccionados para
su revisión?
approve_applies_in_batch:
button: Aprobar todas las respuestas
title: ¿Estás seguro de que deseas aprobar todas las respuestas a este comentario para su revisión?
title: >-
¿Estás seguro de que deseas aprobar todas las respuestas a este
comentario para su revisión?
delete_reply:
title: ¿Estás seguro de que deseas eliminar esta respuesta?
approve_reply:
@ -467,7 +515,9 @@ core:
storage_policies: Políticas de Almacenamiento
empty:
title: No hay adjuntos en el grupo actual.
message: El grupo actual no tiene adjuntos, puedes intentar actualizar o cargar adjuntos.
message: >-
El grupo actual no tiene adjuntos, puedes intentar actualizar o cargar
adjuntos.
actions:
upload: Cargar Adjunto
operations:
@ -497,6 +547,13 @@ core:
items:
grid: Modo Cuadrícula
list: Modo Lista
accept:
label: Tipo
items:
image: Imagen
audio: Audio
video: Video
file: Archivo
detail_modal:
title: "Adjunto: {display_name}"
fields:
@ -530,18 +587,26 @@ core:
delete:
button: Y mover adjunto a sin grupo
title: ¿Estás seguro de que deseas eliminar este grupo?
description: El grupo se eliminará, y los adjuntos bajo el grupo se moverán a sin grupo. Esta operación no se puede deshacer.
description: >-
El grupo se eliminará, y los adjuntos bajo el grupo se moverán a sin
grupo. Esta operación no se puede deshacer.
toast_success: Eliminación exitosa, {total} adjuntos se han movido a sin grupo
delete_with_attachments:
button: También eliminar adjuntos
title: ¿Estás seguro de que deseas eliminar este grupo?
description: Al eliminar el grupo y todos los adjuntos dentro de él, esta acción no se puede deshacer.
toast_success: Eliminación exitosa, {total} adjuntos se han eliminado simultáneamente
description: >-
Al eliminar el grupo y todos los adjuntos dentro de él, esta acción
no se puede deshacer.
toast_success: >-
Eliminación exitosa, {total} adjuntos se han eliminado
simultáneamente
policies_modal:
title: Políticas de Almacenamiento
empty:
title: Actualmente no hay estrategias de almacenamiento disponibles.
message: No hay políticas de almacenamiento disponibles en este momento. Puedes intentar actualizar o crear una nueva política.
message: >-
No hay políticas de almacenamiento disponibles en este momento. Puedes
intentar actualizar o crear una nueva política.
operations:
delete:
title: ¿Estás seguro de que deseas eliminar esta política?
@ -565,7 +630,9 @@ core:
label: "Seleccionar política de almacenamiento:"
empty:
title: Sin política de almacenamiento
description: Antes de cargar, es necesario crear una nueva política de almacenamiento.
description: >-
Antes de cargar, es necesario crear una nueva política de
almacenamiento.
not_select: Por favor, selecciona una política de almacenamiento primero
select_modal:
title: Seleccionar adjunto
@ -574,7 +641,7 @@ core:
label: Adjuntos
operations:
select:
result: "({count} elementos seleccionados)"
result: ({count} elementos seleccionados)
theme:
title: Temas
common:
@ -594,22 +661,35 @@ core:
title: ¿Estás seguro de activar el tema actual?
toast_success: Tema activado exitosamente
reset:
title: ¿Estás seguro de que deseas restablecer todas las configuraciones del tema?
description: Esta operación eliminará la configuración guardada y la restablecerá a los ajustes predeterminados.
title: >-
¿Estás seguro de que deseas restablecer todas las configuraciones del
tema?
description: >-
Esta operación eliminará la configuración guardada y la restablecerá a
los ajustes predeterminados.
toast_success: Configuración restablecida exitosamente
reload:
button: Recargar
title: ¿Estás seguro de que deseas recargar todas las configuraciones del tema?
description: Esta operación solo recargará la configuración del tema y la definición del formulario de ajustes, y no eliminará ninguna configuración guardada.
title: >-
¿Estás seguro de que deseas recargar todas las configuraciones del
tema?
description: >-
Esta operación solo recargará la configuración del tema y la
definición del formulario de ajustes, y no eliminará ninguna
configuración guardada.
toast_success: Recarga de configuración exitosa
uninstall:
title: ¿Estás seguro de que deseas desinstalar este tema?
uninstall_and_delete_config:
button: Desinstalar y eliminar configuración
title: ¿Estás seguro de que deseas desinstalar este tema y su configuración correspondiente?
title: >-
¿Estás seguro de que deseas desinstalar este tema y su configuración
correspondiente?
remote_download:
title: Se ha detectado una dirección de descarga remota, ¿deseas descargar?
description: "Por favor, verifica cuidadosamente si esta dirección es confiable: {url}"
description: >-
Por favor, verifica cuidadosamente si esta dirección es confiable:
{url}
upload_modal:
titles:
install: Instalar tema
@ -633,7 +713,9 @@ core:
not_installed: No Instalados
empty:
title: No hay temas instalados actualmente.
message: No hay temas instalados actualmente, puedes intentar actualizar o instalar un nuevo tema.
message: >-
No hay temas instalados actualmente, puedes intentar actualizar o
instalar un nuevo tema.
not_installed_empty:
title: No hay temas actualmente no instalados.
preview_model:
@ -668,11 +750,15 @@ core:
button: Establecer como menú principal
toast_success: Configuración exitosa
delete_menu:
title: "¿Estás seguro de que deseas eliminar este menú?"
description: Todos los elementos de menú de este menú se eliminarán al mismo tiempo y esta operación no se puede deshacer.
title: ¿Estás seguro de que deseas eliminar este menú?
description: >-
Todos los elementos de menú de este menú se eliminarán al mismo tiempo
y esta operación no se puede deshacer.
delete_menu_item:
title: "¿Estás seguro de que deseas eliminar este elemento de menú?"
description: Todos los subelementos de menú se eliminarán al mismo tiempo y no se pueden restaurar después de la eliminación.
title: ¿Estás seguro de que deseas eliminar este elemento de menú?
description: >-
Todos los subelementos de menú se eliminarán al mismo tiempo y no se
pueden restaurar después de la eliminación.
add_sub_menu_item:
button: Agregar subelemento de menú
list:
@ -699,7 +785,7 @@ core:
placeholder: Selecciona el elemento de menú padre
ref_kind:
label: Tipo
placeholder: "Por favor selecciona {label}"
placeholder: Por favor selecciona {label}
options:
custom: Personalizado
post: Publicación
@ -723,21 +809,29 @@ core:
detail: Detail
empty:
title: There are no installed plugins currently.
message: There are no installed plugins currently, you can try refreshing or installing new plugins.
message: >-
There are no installed plugins currently, you can try refreshing or
installing new plugins.
actions:
install: Install Plugin
operations:
reset:
title: Are you sure you want to reset all configurations of the plugin?
description: This operation will delete the saved configuration and reset it to default settings.
description: >-
This operation will delete the saved configuration and reset it to
default settings.
toast_success: Reset configuration successfully
uninstall:
title: Are you sure you want to uninstall this plugin?
uninstall_and_delete_config:
title: Are you sure you want to uninstall this plugin and its corresponding configuration?
title: >-
Are you sure you want to uninstall this plugin and its corresponding
configuration?
uninstall_when_enabled:
confirm_text: Stop running and uninstall
description: The current plugin is still in the enabled state and will be uninstalled after it stops running. This operation cannot be undone.
description: >-
The current plugin is still in the enabled state and will be
uninstalled after it stops running. This operation cannot be undone.
change_status:
active_title: Are you sure you want to active this plugin?
inactive_title: Are you sure you want to inactive this plugin?
@ -772,7 +866,9 @@ core:
description: Would you like to activate the currently installed plugin?
existed_during_installation:
title: The plugin already exists.
description: The currently installed plugin already exists, do you want to upgrade?
description: >-
The currently installed plugin already exists, do you want to
upgrade?
detail:
title: Plugin detail
header:
@ -801,7 +897,9 @@ core:
identity_authentication: Autenticación de Identidad
empty:
title: Actualmente no hay usuarios que cumplan con los criterios de filtrado.
message: No hay usuarios que coincidan con los criterios de filtrado en este momento. Puedes intentar actualizar o crear un nuevo usuario.
message: >-
No hay usuarios que coincidan con los criterios de filtrado en este
momento. Puedes intentar actualizar o crear un nuevo usuario.
operations:
delete:
title: ¿Estás seguro de que deseas eliminar a este usuario?
@ -869,7 +967,9 @@ core:
button: Vincular
unbind:
button: Desvincular
title: ¿Estás seguro de que deseas desvincular el método de inicio de sesión para {display_name}?
title: >-
¿Estás seguro de que deseas desvincular el método de inicio de
sesión para {display_name}?
fields:
display_name: Nombre para mostrar
username: Nombre de usuario
@ -906,7 +1006,9 @@ core:
operations:
delete:
title: ¿Estás seguro de que deseas eliminar este rol?
description: Una vez eliminado el rol, se eliminarán las asignaciones de rol de los usuarios asociados y esta operación no se puede deshacer.
description: >-
Una vez eliminado el rol, se eliminarán las asignaciones de rol de los
usuarios asociados y esta operación no se puede deshacer.
create_based_on_this_role:
button: Crear basado en este rol
detail:
@ -923,7 +1025,9 @@ core:
creation_time: Fecha de creación
permissions_detail:
system_reserved_alert:
description: El rol reservado del sistema no admite modificaciones. Se recomienda crear un nuevo rol basado en este.
description: >-
El rol reservado del sistema no admite modificaciones. Se recomienda
crear un nuevo rol basado en este.
editing_modal:
titles:
create: Crear rol
@ -940,11 +1044,17 @@ core:
setting: Configuración
operations:
enable:
title: ¿Estás seguro de que deseas habilitar este método de autenticación de identidad?
title: >-
¿Estás seguro de que deseas habilitar este método de autenticación de
identidad?
disable:
title: ¿Estás seguro de que deseas deshabilitar este método de autenticación de identidad?
title: >-
¿Estás seguro de que deseas deshabilitar este método de autenticación
de identidad?
disable_privileged:
tooltip: El método de autenticación reservado por el sistema no se puede deshabilitar
tooltip: >-
El método de autenticación reservado por el sistema no se puede
deshabilitar
detail:
title: Detalle de la autenticación de identidad
fields:
@ -985,7 +1095,11 @@ core:
database: "Base de datos: {database}"
os: "Sistema operativo: {os}"
alert:
external_url_invalid: La URL de acceso externo detectada no coincide con la URL de acceso actual, lo que podría causar que algunos enlaces no se redirijan correctamente. Por favor, verifica la configuración de la URL de acceso externo.
external_url_invalid: >-
La URL de acceso externo detectada no coincide con la URL de acceso
actual, lo que podría causar que algunos enlaces no se redirijan
correctamente. Por favor, verifica la configuración de la URL de acceso
externo.
backup:
title: Copia de Seguridad y Restauración
tabs:
@ -998,14 +1112,19 @@ core:
create:
button: Crear copia de seguridad
title: Crear copia de seguridad
description: ¿Estás seguro de que deseas crear una copia de seguridad? Esta operación puede tomar un tiempo.
description: >-
¿Estás seguro de que deseas crear una copia de seguridad? Esta
operación puede tomar un tiempo.
toast_success: Solicitud de creación de copia de seguridad realizada
delete:
title: Eliminar copia de seguridad
description: ¿Estás seguro de que deseas eliminar esta copia de seguridad?
restore:
title: Restauración exitosa
description: Después de una restauración exitosa, necesitas reiniciar Halo para cargar los recursos del sistema normalmente. Después de hacer clic en OK, reiniciaremos automáticamente Halo.
description: >-
Después de una restauración exitosa, necesitas reiniciar Halo para
cargar los recursos del sistema normalmente. Después de hacer clic en
OK, reiniciaremos automáticamente Halo.
restart:
toast_success: Solicitud de reinicio realizada
list:
@ -1018,9 +1137,15 @@ core:
expiresAt: Expira el {expiresAt}
restore:
tips:
first: 1. El proceso de restauración puede tomar un tiempo, por favor no actualices la página durante este período.
second: 2. Durante el proceso de restauración, aunque los datos existentes no se eliminarán, si hay un conflicto, los datos se sobrescribirán.
third: 3. Después de completar la restauración, necesitas reiniciar Halo para cargar los recursos del sistema normalmente.
first: >-
1. El proceso de restauración puede tomar un tiempo, por favor no
actualices la página durante este período.
second: >-
2. Durante el proceso de restauración, aunque los datos existentes no
se eliminarán, si hay un conflicto, los datos se sobrescribirán.
third: >-
3. Después de completar la restauración, necesitas reiniciar Halo para
cargar los recursos del sistema normalmente.
complete: Restauración completada, esperando reinicio...
start: Iniciar Restauración
exception:
@ -1132,7 +1257,7 @@ core:
extensions:
placeholder:
options:
placeholder: "Ingresa / para seleccionar el tipo de entrada."
placeholder: Ingresa / para seleccionar el tipo de entrada.
toolbox:
attachment: Adjunto
global_search:
@ -1158,7 +1283,9 @@ core:
social_auth_providers:
title: Inicio de sesión de terceros
app_download_alert:
description: "Los temas y complementos para Halo se pueden descargar en las siguientes direcciones:"
description: >-
Los temas y complementos para Halo se pueden descargar en las siguientes
direcciones:
sources:
app_store: "Tienda de aplicaciones oficial: {url}"
github: "GitHub: {url}"
@ -1167,9 +1294,9 @@ core:
toast_recovered: Contenido no guardado recuperado de la caché
formkit:
category_select:
creation_label: "Crear categoría {text}"
creation_label: Crear categoría {text}
tag_select:
creation_label: "Crear etiqueta {text}"
creation_label: Crear etiqueta {text}
validation:
trim: Por favor, elimina los espacios al inicio y al final
common:
@ -1206,7 +1333,7 @@ core:
detail: Detalle
radio:
"yes":
"no": No
"no": "No"
select:
public: Público
private: Privado
@ -1228,10 +1355,10 @@ core:
copy_success: Copiado exitosamente
operation_failed: Fallo en la operación
download_failed: Fallo en la descarga
save_failed_and_retry: "Fallo al guardar, por favor intenta nuevamente"
publish_failed_and_retry: "Fallo al publicar, por favor intenta nuevamente"
network_error: "Error de red, por favor verifica tu conexión"
login_expired: "Sesión expirada, por favor inicia sesión nuevamente"
save_failed_and_retry: Fallo al guardar, por favor intenta nuevamente
publish_failed_and_retry: Fallo al publicar, por favor intenta nuevamente
network_error: Error de red, por favor verifica tu conexión
login_expired: Sesión expirada, por favor inicia sesión nuevamente
forbidden: Acceso denegado
not_found: Recurso no encontrado
server_internal_error: Error interno del servidor
@ -1242,7 +1369,9 @@ core:
warning: Advertencia
descriptions:
cannot_be_recovered: Esta operación es irreversible.
editor_not_found: No se encontró ningún editor que coincida con el formato {raw_type}. Por favor verifica si el complemento del editor ha sido instalado.
editor_not_found: >-
No se encontró ningún editor que coincida con el formato {raw_type}.
Por favor verifica si el complemento del editor ha sido instalado.
filters:
results:
keyword: "Palabra clave: {keyword}"

View File

@ -557,6 +557,13 @@ core:
items:
grid: 网格模式
list: 列表模式
accept:
label: 类型
items:
image: 图片
audio: 音频
video: 视频
file: 文件
detail_modal:
title: 附件:{display_name}
fields:

View File

@ -537,6 +537,13 @@ core:
items:
grid: 網格模式
list: 列表模式
accept:
label: 類型
items:
image: 圖片
audio: 音頻
video: 視頻
file: 文件
detail_modal:
title: 附件:{display_name}
fields: