Skip to content

Commit 1d07847

Browse files
committed
Rewrite listings filters UI
Closes #1058
1 parent 0e85881 commit 1d07847

File tree

5 files changed

+167
-212
lines changed

5 files changed

+167
-212
lines changed

Tekst-Web/i18n/ui/deDE.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ common:
22
general: Allgemein
33
default: standard
44
enabled: Aktiv
5-
on: 'An'
6-
off: 'Aus'
5+
on: 'an'
6+
off: 'aus'
77
from: Von
88
to: Bis
99
status: Status

Tekst-Web/i18n/ui/enUS.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ common:
22
general: General
33
default: default
44
enabled: Enabled
5-
on: 'On'
6-
off: 'Off'
5+
on: 'on'
6+
off: 'off'
77
from: From
88
to: To
99
status: Status
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<script setup lang="ts">
2+
import { FilterIcon, SearchIcon, UndoIcon } from '@/icons';
3+
import { NButton, NCollapse, NCollapseItem, NFlex, NIcon, NInput, NSelect } from 'naive-ui';
4+
import { computed, onMounted } from 'vue';
5+
6+
const props = defineProps<{ flagsLabels: Record<string, string> }>();
7+
const search = defineModel<string | undefined>('search', { required: false, default: undefined });
8+
const flags = defineModel<string[]>('flags', { required: false, default: () => [] });
9+
const emit = defineEmits(['changed']);
10+
11+
const flagsOptions = computed(() =>
12+
Object.entries(props.flagsLabels).map(([k, v]) => ({ label: v, value: k }))
13+
);
14+
const isUnfiltered = computed(
15+
() => !search.value && flags.value.length === Object.keys(props.flagsLabels).length
16+
);
17+
18+
function reset() {
19+
search.value = undefined;
20+
flags.value = Object.entries(props.flagsLabels).map(([k]) => k);
21+
}
22+
23+
function handleFilterCollapseItemClick(data: { name: string; expanded: boolean }) {
24+
if (data.name === 'filters' && !data.expanded) {
25+
reset();
26+
}
27+
}
28+
29+
defineExpose({ reset });
30+
31+
onMounted(() => {
32+
reset();
33+
});
34+
</script>
35+
36+
<template>
37+
<div class="gray-box">
38+
<n-collapse @item-header-click="handleFilterCollapseItemClick">
39+
<n-collapse-item name="filters">
40+
<template #header>
41+
<n-flex align="center" :wrap="false" inline>
42+
<n-icon :component="FilterIcon" class="translucent" />
43+
<span>
44+
{{ $t('common.filters') }}
45+
<template v-if="isUnfiltered">({{ $t('common.off') }})</template>
46+
</span>
47+
</n-flex>
48+
</template>
49+
50+
<n-input
51+
v-model:value="search"
52+
:placeholder="$t('common.searchAction')"
53+
class="mb-md"
54+
round
55+
@update:value="emit('changed')"
56+
>
57+
<template #prefix>
58+
<n-icon :component="SearchIcon" />
59+
</template>
60+
</n-input>
61+
<n-select
62+
v-model:value="flags"
63+
:options="flagsOptions"
64+
multiple
65+
@update:value="emit('changed')"
66+
/>
67+
<n-flex justify="flex-end">
68+
<n-button secondary class="mt-md" @click="reset">
69+
{{ $t('common.reset') }}
70+
<template #icon>
71+
<n-icon :component="UndoIcon" />
72+
</template>
73+
</n-button>
74+
</n-flex>
75+
</n-collapse-item>
76+
</n-collapse>
77+
</div>
78+
</template>

Tekst-Web/src/views/ResourcesView.vue

Lines changed: 45 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,18 @@ import {
1010
} from '@/api';
1111
import { dialogProps } from '@/common';
1212
import HelpButtonWidget from '@/components/HelpButtonWidget.vue';
13-
import LabeledSwitch from '@/components/LabeledSwitch.vue';
13+
import ListingsFilters from '@/components/ListingsFilters.vue';
1414
import IconHeading from '@/components/generic/IconHeading.vue';
1515
import TransferResourceModal from '@/components/modals/TransferResourceModal.vue';
1616
import ResourceListItem from '@/components/resource/ResourceListItem.vue';
1717
import { useMessages } from '@/composables/messages';
1818
import { useTasks } from '@/composables/tasks';
1919
import { $t } from '@/i18n';
20-
import { AddIcon, FilterIcon, ResourceIcon, SearchIcon, UndoIcon } from '@/icons';
20+
import { AddIcon, ResourceIcon } from '@/icons';
2121
import { useAuthStore, useResourcesStore, useStateStore, useUserMessagesStore } from '@/stores';
2222
import { pickTranslation } from '@/utils';
2323
import { createReusableTemplate } from '@vueuse/core';
24-
import {
25-
NButton,
26-
NCollapse,
27-
NCollapseItem,
28-
NFlex,
29-
NIcon,
30-
NInput,
31-
NList,
32-
NPagination,
33-
NSpin,
34-
useDialog,
35-
} from 'naive-ui';
24+
import { NButton, NFlex, NIcon, NList, NPagination, NSpin, useDialog } from 'naive-ui';
3625
import { computed, onMounted, ref } from 'vue';
3726
import { useRouter } from 'vue-router';
3827
@@ -61,44 +50,38 @@ const pagination = ref({
6150
pageSize: 20,
6251
});
6352
64-
const initialFilters = () => ({
65-
search: '',
66-
public: true,
67-
notPublic: true,
68-
proposed: true,
69-
notProposed: true,
70-
ownedByMe: true,
71-
ownedByOthers: true,
72-
hasCorrections: true,
73-
hasNoCorrections: true,
74-
});
75-
76-
const filters = ref(initialFilters());
53+
const filtersRef = ref<InstanceType<typeof ListingsFilters> | null>(null);
54+
const filtersSearch = ref<string>();
55+
const filtersFlags = ref<string[]>();
7756
7857
function filterData(resourcesData: AnyResourceRead[]) {
7958
pagination.value.page = 1;
8059
return resourcesData.filter((r) => {
81-
const resourceStringContent = filters.value.search
60+
const resourceStringContent = filtersSearch.value
8261
? [
8362
r.title.map((t) => t.translation).join(' '),
8463
r.subtitle.map((s) => s.translation).join(' ') || '',
85-
r.ownerId,
64+
r.owner?.name || '',
65+
r.owner?.username || '',
66+
r.owner?.affiliation || '',
8667
r.description.map((d) => d.translation).join(' ') || '',
8768
r.citation,
8869
JSON.stringify(r.meta),
8970
]
90-
.filter((prop) => prop)
71+
.filter(Boolean)
9172
.join(' ')
9273
: '';
9374
return (
94-
(!filters.value.search ||
95-
resourceStringContent.toLowerCase().includes(filters.value.search.toLowerCase())) &&
96-
((filters.value.proposed && r.proposed) || (filters.value.notProposed && !r.proposed)) &&
97-
((filters.value.public && r.public) || (filters.value.notPublic && !r.public)) &&
98-
((filters.value.ownedByMe && r.ownerId === auth.user?.id) ||
99-
(filters.value.ownedByOthers && r.ownerId !== auth.user?.id)) &&
100-
((filters.value.hasCorrections && r.corrections) ||
101-
(filters.value.hasNoCorrections && !r.corrections))
75+
(!filtersSearch.value ||
76+
resourceStringContent.toLowerCase().includes(filtersSearch.value.toLowerCase())) &&
77+
((filtersFlags.value?.includes('proposed') && r.proposed) ||
78+
(filtersFlags.value?.includes('notProposed') && !r.proposed)) &&
79+
((filtersFlags.value?.includes('public') && r.public) ||
80+
(filtersFlags.value?.includes('notPublic') && !r.public)) &&
81+
((filtersFlags.value?.includes('ownedByMe') && r.ownerId === auth.user?.id) ||
82+
(filtersFlags.value?.includes('notOwnedByMe') && r.ownerId !== auth.user?.id)) &&
83+
((filtersFlags.value?.includes('hasCorrections') && r.corrections) ||
84+
(filtersFlags.value?.includes('hasNoCorrections') && !r.corrections))
10285
);
10386
});
10487
}
@@ -146,7 +129,7 @@ async function handleTransferResource(resource?: AnyResourceRead, user?: UserRea
146129
})
147130
);
148131
}
149-
filters.value = initialFilters();
132+
filtersRef.value?.reset();
150133
showTransferModal.value = false;
151134
transferTargetResource.value = undefined;
152135
actionsLoading.value = false;
@@ -171,7 +154,7 @@ function handleProposeClick(resource: AnyResourceRead) {
171154
$t('resources.msgProposed', { title: pickTranslation(resource.title, state.locale) })
172155
);
173156
}
174-
filters.value = initialFilters();
157+
filtersRef.value?.reset();
175158
actionsLoading.value = false;
176159
},
177160
});
@@ -196,7 +179,7 @@ function handleUnproposeClick(resource: AnyResourceRead) {
196179
$t('resources.msgUnproposed', { title: pickTranslation(resource.title, state.locale) })
197180
);
198181
}
199-
filters.value = initialFilters();
182+
filtersRef.value?.reset();
200183
actionsLoading.value = false;
201184
},
202185
});
@@ -221,7 +204,7 @@ function handlePublishClick(resource: AnyResourceRead) {
221204
$t('resources.msgPublished', { title: pickTranslation(resource.title, state.locale) })
222205
);
223206
}
224-
filters.value = initialFilters();
207+
filtersRef.value?.reset();
225208
actionsLoading.value = false;
226209
},
227210
});
@@ -246,7 +229,7 @@ function handleUnpublishClick(resource: AnyResourceRead) {
246229
$t('resources.msgUnpublished', { title: pickTranslation(resource.title, state.locale) })
247230
);
248231
}
249-
filters.value = initialFilters();
232+
filtersRef.value?.reset();
250233
actionsLoading.value = false;
251234
},
252235
});
@@ -376,12 +359,6 @@ function handleReqVersionIntegrationClick(resourceVersion: AnyResourceRead) {
376359
userMessages.openConversation(originalResource.ownerId, prepMsg);
377360
}
378361
379-
function handleFilterCollapseItemClick(data: { name: string; expanded: boolean }) {
380-
if (data.name === 'filters' && !data.expanded) {
381-
filters.value = initialFilters();
382-
}
383-
}
384-
385362
onMounted(() => {
386363
// inform user in case there are corrections for resources of another text
387364
if (
@@ -416,51 +393,24 @@ onMounted(() => {
416393

417394
<template v-if="resources.ofText && !resources.error && !loading">
418395
<!-- Filters -->
419-
<n-collapse class="mb-lg" @item-header-click="handleFilterCollapseItemClick">
420-
<n-collapse-item name="filters">
421-
<template #header>
422-
<n-flex align="center" :wrap="false">
423-
<n-icon :component="FilterIcon" class="translucent" />
424-
<span>{{ $t('common.filters') }}</span>
425-
</n-flex>
426-
</template>
427-
428-
<n-flex vertical size="small" class="gray-box">
429-
<n-input
430-
v-model:value="filters.search"
431-
:placeholder="$t('common.searchAction')"
432-
class="mb-md"
433-
round
434-
>
435-
<template #prefix>
436-
<n-icon :component="SearchIcon" />
437-
</template>
438-
</n-input>
439-
<labeled-switch v-model="filters.public" :label="$t('resources.public')" />
440-
<labeled-switch v-model="filters.notPublic" :label="$t('resources.notPublic')" />
441-
<labeled-switch v-model="filters.proposed" :label="$t('resources.proposed')" />
442-
<labeled-switch v-model="filters.notProposed" :label="$t('resources.notProposed')" />
443-
<labeled-switch v-model="filters.ownedByMe" :label="$t('resources.ownedByMe')" />
444-
<labeled-switch v-model="filters.ownedByOthers" :label="$t('resources.ownedByOthers')" />
445-
<labeled-switch
446-
v-model="filters.hasCorrections"
447-
:label="$t('resources.hasCorrections')"
448-
/>
449-
<labeled-switch
450-
v-model="filters.hasNoCorrections"
451-
:label="$t('resources.hasNoCorrections')"
452-
/>
453-
<n-button secondary class="mt-md" @click="filters = initialFilters()">
454-
{{ $t('common.reset') }}
455-
<template #icon>
456-
<n-icon :component="UndoIcon" />
457-
</template>
458-
</n-button>
459-
</n-flex>
460-
</n-collapse-item>
461-
</n-collapse>
462-
463-
<div class="resource-list-header">
396+
<listings-filters
397+
ref="filtersRef"
398+
v-model:search="filtersSearch"
399+
v-model:flags="filtersFlags"
400+
:flags-labels="{
401+
public: $t('resources.public'),
402+
notPublic: $t('resources.notPublic'),
403+
proposed: $t('resources.proposed'),
404+
notProposed: $t('resources.notProposed'),
405+
ownedByMe: $t('resources.ownedByMe'),
406+
ownedByOthers: $t('resources.ownedByOthers'),
407+
hasCorrections: $t('resources.hasCorrections'),
408+
hasNoCorrections: $t('resources.hasNoCorrections'),
409+
}"
410+
/>
411+
412+
<!-- List Header -->
413+
<n-flex justify="space-between" align="center">
464414
<div class="text-small translucent ellipsis">
465415
{{
466416
$t('resources.msgFoundCount', {
@@ -480,7 +430,7 @@ onMounted(() => {
480430
</template>
481431
{{ $t('resources.new') }}
482432
</n-button>
483-
</div>
433+
</n-flex>
484434

485435
<!-- Resources List -->
486436
<div class="content-block">
@@ -532,14 +482,6 @@ onMounted(() => {
532482
</template>
533483

534484
<style scoped>
535-
.resource-list-header {
536-
display: flex;
537-
justify-content: space-between;
538-
flex-wrap: nowrap;
539-
align-items: flex-end;
540-
max-width: 100%;
541-
}
542-
543485
.pagination-container:first-child {
544486
margin-bottom: var(--gap-lg);
545487
}

0 commit comments

Comments
 (0)