Skip to content

Commit

Permalink
feat: add tags filter feature (#53)
Browse files Browse the repository at this point in the history
应用市场筛选项支持通过标签筛选。

<img width="1665" alt="图片" src="https://github.com/halo-dev/plugin-app-store/assets/21301288/3bfd3966-2923-4baa-a4cf-073d56f12e5b">
<img width="1664" alt="图片" src="https://github.com/halo-dev/plugin-app-store/assets/21301288/576a615b-c18f-4a8c-b8d1-9fb080e09ffa">

[plugin-app-store-1.0.0-SNAPSHOT.jar.zip](https://github.com/halo-dev/plugin-app-store/files/13971569/plugin-app-store-1.0.0-SNAPSHOT.jar.zip)

/kind feature

```release-note
支持通过标签筛选应用
```
  • Loading branch information
ruibaby committed Jan 18, 2024
1 parent 9e7bd05 commit 4a8cefb
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 7 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -41,6 +41,6 @@ build {
}

halo {
version = '2.10.2'
version = '2.11'
debug = true
}
35 changes: 33 additions & 2 deletions console/src/components/AppStoreTab.vue
Expand Up @@ -17,7 +17,7 @@ import AppCard from "./AppCard.vue";
import { useLocalStorage } from "@vueuse/core";
import AppDetailModal from "./AppDetailModal.vue";
import { nextTick } from "vue";
import type { ApplicationSearchResult, ListResponse } from "@/types";
import type { ApplicationSearchResult, ApplicationTag, ListResponse } from "@/types";
import storeApiClient from "@/utils/store-api-client";
import AgreementsModal from "./AgreementsModal.vue";
Expand Down Expand Up @@ -48,9 +48,26 @@ const page = ref(1);
const size = ref(20);
const selectedSort = ref("latestReleaseTimestamp,desc");
const selectedPriceMode = ref();
const selectedTag = ref();
const { data: tags } = useQuery<ApplicationTag[]>({
queryKey: ["app-tags"],
queryFn: async () => {
const { data } = await storeApiClient.get<ListResponse<ApplicationTag>>("/apis/store.halo.run/v1alpha1/tags");
if (data?.items.length) {
// sort by privileged<boolean>
return data.items.sort((a: ApplicationTag, b: ApplicationTag) => {
return +b.spec.privileged - +a.spec.privileged;
});
}
return [];
},
});
const { data, isFetching, isLoading, refetch } = useQuery<ListResponse<ApplicationSearchResult>>({
queryKey: ["store-apps", keyword, selectedSort, page, size, selectedPriceMode, props.type],
queryKey: ["store-apps", keyword, selectedSort, page, size, selectedPriceMode, props.type, selectedTag],
queryFn: async () => {
const { data } = await storeApiClient.get<ListResponse<ApplicationSearchResult>>(
`/apis/api.store.halo.run/v1alpha1/applications`,
Expand All @@ -62,6 +79,7 @@ const { data, isFetching, isLoading, refetch } = useQuery<ListResponse<Applicati
size: size.value,
priceMode: selectedPriceMode.value,
type: props.type,
tags: selectedTag.value || undefined,
},
}
);
Expand Down Expand Up @@ -149,6 +167,19 @@ watch([selectedPriceMode, selectedSort, keyword], () => {
},
]"
/>
<FilterDropdown
v-model="selectedTag"
label="标签"
:items="[
{
label: '全部',
},
...(tags?.map((tag) => ({
value: tag.metadata.name,
label: tag.spec.displayName,
})) || []),
]"
/>
<FilterDropdown
v-model="selectedSort"
:label="$t('core.common.filters.labels.sort')"
Expand Down
24 changes: 24 additions & 0 deletions console/src/components/AppTag.vue
@@ -0,0 +1,24 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
selected: boolean;
}>(),
{
selected: false,
}
);
</script>

<template>
<span class="plugin-app-store-tag" :class="{ active: selected }"> <slot /> </span>
</template>

<style scoped>
.plugin-app-store-tag {
@apply as-inline-flex as-cursor-pointer as-select-none as-items-center as-rounded-md as-bg-gray-50 as-px-2 as-py-1 as-text-xs as-font-medium as-text-gray-600 as-ring-1 as-ring-inset as-ring-gray-500/10;
}
.plugin-app-store-tag.active {
@apply !as-bg-blue-600 !as-text-white !as-ring-blue-600;
}
</style>
45 changes: 43 additions & 2 deletions console/src/components/detail/DetailSidebar.vue
@@ -1,14 +1,44 @@
<script lang="ts" setup>
import type { ApplicationDetail } from "@/types";
import type { ApplicationDetail, ApplicationTag, ListResponse } from "@/types";
import { relativeTimeTo } from "@/utils/date";
withDefaults(
import storeApiClient from "@/utils/store-api-client";
import { useQuery } from "@tanstack/vue-query";
import { computed } from "vue";
import AppTag from "../AppTag.vue";
const props = withDefaults(
defineProps<{
app?: ApplicationDetail;
}>(),
{
app: undefined,
}
);
const { data: tags, isLoading } = useQuery<ApplicationTag[]>({
queryKey: ["app-tags"],
queryFn: async () => {
const { data } = await storeApiClient.get<ListResponse<ApplicationTag>>("/apis/store.halo.run/v1alpha1/tags");
if (data?.items.length) {
// sort by privileged<boolean>
return data.items.sort((a: ApplicationTag, b: ApplicationTag) => {
return +b.spec.privileged - +a.spec.privileged;
});
}
return [];
},
});
const currentTags = computed(() => {
if (!props.app || !tags.value?.length) {
return [];
}
return tags.value.filter((tag) => {
return props.app?.application.spec.tags?.includes(tag.metadata.name);
});
});
</script>

<template>
Expand Down Expand Up @@ -94,6 +124,17 @@ withDefaults(
</div>
</div>
</li>
<li v-if="currentTags.length" class="as-flex as-py-4">
<div class="as-space-y-2">
<h2 class="as-text-base as-font-medium as-text-gray-900">标签</h2>
<div class="as-flex as-flex-wrap as-gap-2">
<span v-if="isLoading" class="as-text-sm as-text-gray-600">加载中...</span>
<AppTag v-for="tag in currentTags" v-else :key="tag.metadata.name">
{{ tag.spec.displayName }}
</AppTag>
</div>
</div>
</li>
<li class="as-flex as-py-4">
<div class="as-space-y-2">
<h2 class="as-text-base as-font-medium as-text-gray-900">更多</h2>
Expand Down
53 changes: 51 additions & 2 deletions console/src/views/AppStore.vue
Expand Up @@ -5,7 +5,7 @@ import AppDetailModal from "@/components/AppDetailModal.vue";
import { useFetchInstalledPlugins } from "@/composables/use-plugin";
import { useFetchInstalledThemes } from "@/composables/use-theme";
import { STORE_APP_ID } from "@/constant";
import type { ApplicationSearchResult, ListResponse } from "@/types";
import type { ApplicationSearchResult, ApplicationTag, ListResponse } from "@/types";
import storeApiClient from "@/utils/store-api-client";
import {
IconArrowLeft,
Expand All @@ -27,6 +27,7 @@ import { computed, nextTick, watch } from "vue";
import { ref } from "vue";
import RiApps2Line from "~icons/ri/apps-2-line";
import { useRouteQuery } from "@vueuse/router";
import AppTag from "@/components/AppTag.vue";
const Types = [
{
Expand Down Expand Up @@ -77,12 +78,38 @@ const size = useRouteQuery<number>("size", 20, { transform: Number });
const selectedSort = useRouteQuery<string | undefined>("sort", "latestReleaseTimestamp,desc");
const selectedPriceMode = useRouteQuery("price-mode");
const selectedType = useRouteQuery<string | undefined>("type");
const selectedTag = useRouteQuery<string | undefined>("tag", "");
const onlyQueryInstalled = useRouteQuery<string>("installed", "false");
const onlyQueryInstalledAsBoolean = computed(() => onlyQueryInstalled.value === "true");
const { installedPlugins } = useFetchInstalledPlugins(onlyQueryInstalledAsBoolean);
const { installedThemes } = useFetchInstalledThemes(onlyQueryInstalledAsBoolean);
const { data: tags } = useQuery<ApplicationTag[]>({
queryKey: ["app-tags"],
queryFn: async () => {
const { data } = await storeApiClient.get<ListResponse<ApplicationTag>>("/apis/store.halo.run/v1alpha1/tags");
if (data?.items.length) {
// sort by privileged<boolean>
return data.items.sort((a: ApplicationTag, b: ApplicationTag) => {
return +b.spec.privileged - +a.spec.privileged;
});
}
return [];
},
});
function handleSetTag(tagName: string) {
// if tag is selected, clear it
if (selectedTag.value === tagName) {
selectedTag.value = "";
return;
}
selectedTag.value = tagName;
}
const { data, isFetching, isLoading, refetch } = useQuery<ListResponse<ApplicationSearchResult>>({
queryKey: [
"store-apps",
Expand All @@ -92,6 +119,7 @@ const { data, isFetching, isLoading, refetch } = useQuery<ListResponse<Applicati
size,
selectedPriceMode,
selectedType,
selectedTag,
onlyQueryInstalledAsBoolean,
],
queryFn: async () => {
Expand Down Expand Up @@ -123,6 +151,7 @@ const { data, isFetching, isLoading, refetch } = useQuery<ListResponse<Applicati
priceMode: selectedPriceMode.value,
type: selectedType.value,
names: appIds,
tags: selectedTag.value || undefined,
},
}
);
Expand Down Expand Up @@ -186,7 +215,7 @@ const handleSelectNext = async () => {
};
// page refresh
watch([selectedPriceMode, selectedType, selectedSort, onlyQueryInstalled, keyword], () => {
watch([selectedPriceMode, selectedType, selectedSort, onlyQueryInstalled, keyword, selectedTag], () => {
page.value = 1;
});
</script>
Expand Down Expand Up @@ -327,6 +356,26 @@ watch([selectedPriceMode, selectedType, selectedSort, onlyQueryInstalled, keywor
</div>
</div>
</li>
<li className="as-flex as-py-4">
<div className="as-space-y-2">
<h2 className="as-text-base as-font-medium as-text-gray-900">标签</h2>
<div>
<fieldset className="as-mt-4">
<div className="as-flex as-flex-wrap as-gap-2">
<AppTag :selected="!selectedTag" @click="handleSetTag('')"> 全部 </AppTag>
<AppTag
v-for="tag in tags"
:key="tag.metadata.name"
:selected="tag.metadata.name === selectedTag"
@click="handleSetTag(tag.metadata.name)"
>
{{ tag.spec.displayName }}
</AppTag>
</div>
</fieldset>
</div>
</div>
</li>
</ul>
</aside>
<div class="as-col-span-12 sm:as-col-span-10">
Expand Down

0 comments on commit 4a8cefb

Please sign in to comment.