Frontend filter menu

This commit is contained in:
Brieuc Dubois 2024-01-10 17:56:31 +01:00
parent c725d5154e
commit ca3811a48b
6 changed files with 246 additions and 16 deletions

View File

@ -5,7 +5,7 @@ async function create(
viewId: number,
projectTagId: number,
filterType: number,
tagOptionId: number
tagOptionId: number | null
): Promise<number | null> {
const response = await api.post(`/v1/filters`, {
view_id: viewId,
@ -27,7 +27,7 @@ async function update(
viewId: number,
projectTagId: number,
filterType: number,
tagOptionId: number
tagOptionId: number | null
): Promise<boolean> {
const response = await api.put(`/v1/filters/${filterId}`, {
view_id: viewId,

View File

@ -0,0 +1,18 @@
<script lang="ts">
import type Filter from '$lib/types/Filter';
import FilterMenuItem from './FilterMenuItem.svelte';
import Menu from './Menu.svelte';
export let isOpen = false;
export let filters: Filter[] = [];
</script>
<Menu bind:isOpen>
{#each filters as filter}
<FilterMenuItem {filter} />
{/each}
<FilterMenuItem />
</Menu>
<style lang="less">
</style>

View File

@ -0,0 +1,155 @@
<script lang="ts">
import currentView from '$lib/stores/currentView';
import Filter from '$lib/types/Filter';
import ProjectTag, { projectTags } from '$lib/types/ProjectTag';
import type TagOption from '$lib/types/TagOption';
import Menu from './Menu.svelte';
export let filter: Filter | null = null;
let isProjectTagOpen = false;
let isFilterTypeOpen = false;
let isOptionOpen = false;
async function selectProjectTag(projectTag: ProjectTag) {
if (!$currentView) return;
if (!filter) {
filter = await $currentView?.addFilter(projectTag, 0, null);
return;
}
if (filter.projectTag.id !== projectTag.id) {
const res = await filter.setProjectTag(projectTag);
if (!res) return;
currentView.reload();
}
isProjectTagOpen = false;
}
async function selectFilterType(filterType: number) {
if (!filter) return;
if (filter.filterType !== filterType) {
const res = await filter.setFilterType(filterType);
if (!res) return;
currentView.reload();
}
isFilterTypeOpen = false;
}
async function selectOption(option: TagOption) {
if (!filter) return;
if (filter.tagOption !== option) {
const res = await filter.setTagOption(option);
if (!res) return;
currentView.reload();
}
isOptionOpen = false;
}
</script>
<div class="item">
<div>
<div
class="part"
on:click={() => (isProjectTagOpen = !isProjectTagOpen)}
on:keydown={(e) => {
if (e.key === 'Enter') isProjectTagOpen = !isProjectTagOpen;
}}
tabindex="0"
role="button"
>
{#if filter}
{filter.projectTag.title}
{/if}
</div>
<Menu bind:isOpen={isProjectTagOpen}>
{#each $projectTags as projectTag}
<button on:click={() => selectProjectTag(projectTag)}>
{projectTag.title}
</button>
{/each}
</Menu>
</div>
<div>
<div
class="part"
on:click={() => (isFilterTypeOpen = !isFilterTypeOpen)}
on:keydown={(e) => {
if (e.key === 'Enter') isFilterTypeOpen = !isFilterTypeOpen;
}}
tabindex="0"
role="button"
>
{#if filter}
{filter.filterType}
{/if}
</div>
{#if filter}
<Menu bind:isOpen={isFilterTypeOpen}>
<button on:click={() => selectFilterType(0)}> is </button>
<button
on:click={() => {
selectFilterType(1);
isFilterTypeOpen = false;
}}
>
is not
</button>
</Menu>
{/if}
</div>
<div>
<div
class="part"
on:click={() => (isOptionOpen = !isOptionOpen)}
on:keydown={(e) => {
if (e.key === 'Enter') isOptionOpen = !isOptionOpen;
}}
tabindex="0"
role="button"
>
{#if filter && filter.tagOption}
{filter.tagOption.value}
{/if}
</div>
{#if filter && filter.projectTag}
<Menu bind:isOpen={isOptionOpen}>
{#each filter.projectTag.options as option}
<button on:click={() => selectOption(option)}>
{option.value}
</button>
{/each}
</Menu>
{/if}
</div>
</div>
<style lang="less">
.item {
display: flex;
flex-direction: row;
}
.part {
min-width: 50px;
height: 30px;
margin: 5px;
text-align: center;
line-height: 30px;
padding: 0 5px;
&:hover {
background-color: #fff2;
border-radius: 5px;
}
}
button {
min-width: 100px;
text-align: left;
padding: 5px;
margin: 2px 5px;
}
</style>

View File

@ -7,10 +7,12 @@
import type ProjectTag from '$lib/types/ProjectTag';
import { projectTags } from '$lib/types/ProjectTag';
import { get } from 'svelte/store';
import FilterMenu from '../menu/FilterMenu.svelte';
export let project: Project;
let groupMenuOpen = false;
let sortMenuOpen = false;
let filterMenuOpen = false;
async function setGroup(projectTag: ProjectTag): Promise<boolean> {
const view = get(currentView);
@ -54,7 +56,10 @@
/>
</div>
<button class:disabled={true}>Sub-group</button>
<button class:disabled={true}>Filter</button>
<div>
<button on:click={() => (filterMenuOpen = !filterMenuOpen)}>Filter</button>
<FilterMenu bind:isOpen={filterMenuOpen} filters={$currentView?.filters} />
</div>
<div>
<button on:click={() => (sortMenuOpen = !sortMenuOpen)} class:defined={$currentView?.sortTag}>
Sort

View File

@ -10,14 +10,14 @@ export default class Filter {
private _view: View;
private _projectTag: ProjectTag;
private _filterType: number;
private _tagOption: TagOption;
private _tagOption: TagOption | null;
private constructor(
id: number,
view: View,
projectTag: ProjectTag,
filterType: number,
tagOption: TagOption
tagOption: TagOption | null
) {
this._id = id;
this._view = view;
@ -42,7 +42,7 @@ export default class Filter {
return this._filterType;
}
get tagOption(): TagOption {
get tagOption(): TagOption | null {
return this._tagOption;
}
@ -50,9 +50,9 @@ export default class Filter {
view: View,
projectTag: ProjectTag,
filterType: number,
tagOption: TagOption
tagOption: TagOption | null
): Promise<Filter | null> {
const id = await filtersApi.create(view.id, projectTag.id, filterType, tagOption.id);
const id = await filtersApi.create(view.id, projectTag.id, filterType, tagOption?.id || null);
if (!id) return null;
return new Filter(id, view, projectTag, filterType, tagOption);
@ -62,6 +62,54 @@ export default class Filter {
return await filtersApi.delete(this.id);
}
async setProjectTag(projectTag: ProjectTag): Promise<boolean> {
const res = await filtersApi.update(
this.id,
this.view.id,
projectTag.id,
this.filterType,
this.tagOption?.id || null
);
if (!res) return false;
this._projectTag = projectTag;
return true;
}
async setFilterType(filterType: number): Promise<boolean> {
const res = await filtersApi.update(
this.id,
this.view.id,
this.projectTag.id,
filterType,
this.tagOption?.id || null
);
if (!res) return false;
this._filterType = filterType;
return true;
}
async setTagOption(tagOption: TagOption | null): Promise<boolean> {
const res = await filtersApi.update(
this.id,
this.view.id,
this.projectTag.id,
this.filterType,
tagOption?.id || null
);
if (!res) return false;
this._tagOption = tagOption;
return true;
}
static parseAll(json: any): Filter[];
static parseAll(json: any, view: View | null): Filter[];
@ -100,11 +148,7 @@ export default class Filter {
}
const tagOption = projectTag.options.find((option) => option.id === json.option_id);
if (!tagOption) {
toastAlert('Failed to parse filter: tagOption not found');
return null;
}
return new Filter(json.id, view, projectTag, json.filter_type, tagOption);
return new Filter(json.id, view, projectTag, json.filter_type, tagOption || null);
}
}

View File

@ -66,6 +66,10 @@ export default class View {
return this._sortDirection;
}
get filters(): Filter[] {
return this._filters;
}
static fromId(id: number): View | null {
for (const view of get(views)) {
if (view.id === id) {
@ -152,13 +156,17 @@ export default class View {
return true;
}
async addFilter(projectTag: ProjectTag, filterType: number, option: TagOption): Promise<boolean> {
async addFilter(
projectTag: ProjectTag,
filterType: number,
option: TagOption | null
): Promise<Filter | null> {
const filter = await Filter.create(this, projectTag, filterType, option);
if (!filter) return false;
if (!filter) return null;
this._filters = [...this._filters, filter];
return true;
return filter;
}
async removeFilter(filter: Filter): Promise<boolean> {