Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tags filter feature #53

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading