Skip to content

Commit

Permalink
feat: add labels filter to proposals list (#990)
Browse files Browse the repository at this point in the history
  • Loading branch information
wa0x6e authored Nov 27, 2024
1 parent 1a0efa2 commit 6597ff6
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 21 deletions.
6 changes: 5 additions & 1 deletion apps/ui/src/components/EditorLabels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ const labels = defineModel<string[]>({

<template>
<div v-if="space.labels?.length">
<PickerLabel v-model="labels" :labels="space.labels">
<PickerLabel
v-model="labels"
:labels="space.labels"
:button-props="{ class: 'outline-none focus-within:text-skin-link' }"
>
<template #button>
<div class="flex justify-between items-center mb-2.5">
<h4 class="eyebrow" v-text="'Labels'" />
Expand Down
17 changes: 12 additions & 5 deletions apps/ui/src/components/PickerLabel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { SpaceMetadataLabel } from '@/types';
const props = defineProps<{
labels: SpaceMetadataLabel[];
buttonProps?: Record<string, any>;
panelProps?: Record<string, any>;
}>();
defineSlots<{
button(props: { close: () => void }): any;
}>();
const selectedLabels = defineModel<string[]>({
Expand All @@ -32,16 +38,16 @@ const filteredLabels = computed(() =>
</script>

<template>
<Popover v-slot="{ open }" class="relative contents">
<Popover v-slot="{ open, close }" class="relative contents">
<PopoverButton
class="outline-none focus-within:text-skin-link w-full"
class="w-full"
:class="open ? 'text-skin-link' : 'text-skin-text'"
v-bind="buttonProps"
>
<slot name="button">
<slot name="button" :close="close">
<IH-pencil />
</slot>
</PopoverButton>

<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
Expand All @@ -52,8 +58,9 @@ const filteredLabels = computed(() =>
>
<PopoverPanel
focus
class="absolute z-10 left-0 -mt-2 mx-4 pb-3"
class="absolute z-[11] left-0 -mt-2 mx-4 pb-3"
style="width: calc(100% - 48px)"
v-bind="panelProps"
>
<Combobox
v-slot="{ activeOption }"
Expand Down
13 changes: 11 additions & 2 deletions apps/ui/src/networks/common/graphqlApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,10 @@ export function createApi(
filters?: ProposalsFilter,
searchQuery = ''
): Promise<Proposal[]> => {
const _filters: Record<string, any> = clone(filters || {});
const _filters: ProposalsFilter = clone(filters || {});
const metadataFilters: Record<string, any> = {
title_contains_nocase: searchQuery
};
const state = _filters.state;

if (state === 'active') {
Expand All @@ -465,6 +468,12 @@ export function createApi(

delete _filters.state;

if (_filters.labels?.length) {
metadataFilters.labels_contains = _filters.labels;
}

delete _filters.labels;

const { data } = await apollo.query({
query: PROPOSALS_QUERY,
variables: {
Expand All @@ -473,7 +482,7 @@ export function createApi(
where: {
space_in: spaceIds,
cancelled: false,
metadata_: { title_contains_nocase: searchQuery },
metadata_: metadataFilters,
..._filters
}
}
Expand Down
8 changes: 7 additions & 1 deletion apps/ui/src/networks/offchain/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export function createApi(
filters?: ProposalsFilter,
searchQuery = ''
): Promise<Proposal[]> => {
const _filters: Record<string, any> = clone(filters || {});
const _filters: ProposalsFilter = clone(filters || {});
const state = _filters.state;

if (state === 'active') {
Expand All @@ -517,6 +517,12 @@ export function createApi(

delete _filters.state;

if (_filters.labels?.length) {
_filters.labels_in = _filters.labels;
}

delete _filters.labels;

const { data } = await apollo.query({
query: PROPOSALS_QUERY,
variables: {
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type SpacesFilter = {
};
export type ProposalsFilter = {
state?: 'any' | 'active' | 'pending' | 'closed';
labels?: string[];
} & Record<string, any>;
export type Connector =
| 'argentx'
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/stores/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const useProposalsStore = defineStore('proposals', () => {
async function fetch(
spaceId: string,
networkId: NetworkID,
state?: ProposalsFilter['state']
filters?: ProposalsFilter
) {
await metaStore.fetchBlock(networkId);

Expand Down Expand Up @@ -96,7 +96,7 @@ export const useProposalsStore = defineStore('proposals', () => {
limit: PROPOSALS_LIMIT
},
metaStore.getCurrent(networkId) || 0,
{ state }
filters
)
);

Expand Down
102 changes: 92 additions & 10 deletions apps/ui/src/views/Space/Proposals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const route = useRoute();
const proposalsStore = useProposalsStore();
const state = ref<NonNullable<ProposalsFilter['state']>>('any');
const labels = ref<string[]>([]);
const selectIconBaseProps = {
size: 16
Expand All @@ -27,6 +28,17 @@ const proposalsRecord = computed(
() => proposalsStore.proposals[`${props.space.network}:${props.space.id}`]
);
const spaceLabels = computed(() => {
if (!props.space.labels) return {};
return Object.fromEntries(props.space.labels.map(label => [label.id, label]));
});
function handleClearLabelsFilter(close: () => void) {
labels.value = [];
close();
}
async function handleEndReached() {
if (!proposalsRecord.value?.hasMoreProposals) return;
Expand All @@ -37,25 +49,42 @@ function handleFetchVotingPower() {
fetchVotingPower(props.space);
}
watch(
[() => route.query.state as string],
([toState]) => {
watchThrottled(
[
() => route.query.state as string,
() => route.query.labels as string[] | string
],
([toState, toLabels]) => {
state.value = ['any', 'active', 'pending', 'closed'].includes(toState)
? (toState as NonNullable<ProposalsFilter['state']>)
: 'any';
let normalizedLabels = toLabels || [];
normalizedLabels = Array.isArray(normalizedLabels)
? normalizedLabels
: [normalizedLabels];
labels.value = normalizedLabels.filter(id => spaceLabels.value[id]);
proposalsStore.reset(props.space.id, props.space.network);
proposalsStore.fetch(props.space.id, props.space.network, state.value);
proposalsStore.fetch(props.space.id, props.space.network, {
state: state.value,
labels: labels.value
});
},
{ immediate: true }
{ throttle: 1000, immediate: true }
);
watch(
[props.space, state],
([toSpace, toState], [fromSpace, fromState]) => {
if (toSpace.id !== fromSpace?.id || toState !== fromState) {
[props.space, state, labels],
([toSpace, toState, toLabels], [fromSpace, fromState, fromLabels]) => {
if (
toSpace.id !== fromSpace?.id ||
toState !== fromState ||
toLabels !== fromLabels
) {
const query: LocationQueryRaw = {
...route.query,
state: toState === 'any' ? undefined : toState
state: toState === 'any' ? undefined : toState,
labels: !toLabels?.length ? undefined : toLabels
};
router.push({ query });
Expand Down Expand Up @@ -83,7 +112,10 @@ watchEffect(() => setTitle(`Proposals - ${props.space.name}`));

<template>
<div>
<div class="flex justify-between p-4 gap-2">
<div
class="flex justify-between p-4 gap-2 gap-y-3 flex-row"
:class="{ 'flex-col-reverse sm:flex-row': space.labels?.length }"
>
<div class="flex gap-2">
<UiSelectDropdown
v-model="state"
Expand Down Expand Up @@ -115,6 +147,56 @@ watchEffect(() => setTitle(`Proposals - ${props.space.name}`));
}
]"
/>
<div v-if="space.labels?.length" class="sm:relative">
<PickerLabel
v-model="labels"
:labels="space.labels"
:button-props="{
class: [
'flex items-center gap-2 relative rounded-full leading-[100%] min-w-[75px] max-w-[230px] border button h-[42px] top-1 text-skin-link bg-skin-bg'
]
}"
:panel-props="{ class: 'sm:min-w-[290px] sm:ml-0 !mt-3' }"
>
<template #button="{ close }">
<div
class="absolute top-[-10px] bg-skin-bg px-1 left-2.5 text-sm text-skin-text"
>
Labels
</div>
<div
v-if="labels.length"
class="flex gap-1 mx-2.5 overflow-hidden items-center"
>
<ul v-if="labels.length" class="flex gap-1 mr-4">
<li v-for="id in labels" :key="id">
<UiProposalLabel
:label="spaceLabels[id].name"
:color="spaceLabels[id].color"
/>
</li>
</ul>
<div
class="flex items-center absolute rounded-r-full right-[1px] pr-2 h-[23px] bg-skin-bg"
>
<div
class="block w-2 -ml-2 h-full bg-gradient-to-l from-skin-bg"
/>
<button
v-if="labels.length"
class="text-skin-text rounded-full hover:text-skin-link"
title="Clear all labels"
@click.stop="handleClearLabelsFilter(close)"
@keydown.enter.stop="handleClearLabelsFilter(close)"
>
<IH-x-circle size="16" />
</button>
</div>
</div>
<span v-else class="px-3 text-skin-link">Any</span>
</template>
</PickerLabel>
</div>
</div>
<div class="flex gap-2 truncate">
<IndicatorVotingPower
Expand Down

0 comments on commit 6597ff6

Please sign in to comment.