Frontend filter menu
This commit is contained in:
parent
c725d5154e
commit
ca3811a48b
|
@ -5,7 +5,7 @@ async function create(
|
||||||
viewId: number,
|
viewId: number,
|
||||||
projectTagId: number,
|
projectTagId: number,
|
||||||
filterType: number,
|
filterType: number,
|
||||||
tagOptionId: number
|
tagOptionId: number | null
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const response = await api.post(`/v1/filters`, {
|
const response = await api.post(`/v1/filters`, {
|
||||||
view_id: viewId,
|
view_id: viewId,
|
||||||
|
@ -27,7 +27,7 @@ async function update(
|
||||||
viewId: number,
|
viewId: number,
|
||||||
projectTagId: number,
|
projectTagId: number,
|
||||||
filterType: number,
|
filterType: number,
|
||||||
tagOptionId: number
|
tagOptionId: number | null
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const response = await api.put(`/v1/filters/${filterId}`, {
|
const response = await api.put(`/v1/filters/${filterId}`, {
|
||||||
view_id: viewId,
|
view_id: viewId,
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -7,10 +7,12 @@
|
||||||
import type ProjectTag from '$lib/types/ProjectTag';
|
import type ProjectTag from '$lib/types/ProjectTag';
|
||||||
import { projectTags } from '$lib/types/ProjectTag';
|
import { projectTags } from '$lib/types/ProjectTag';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import FilterMenu from '../menu/FilterMenu.svelte';
|
||||||
|
|
||||||
export let project: Project;
|
export let project: Project;
|
||||||
let groupMenuOpen = false;
|
let groupMenuOpen = false;
|
||||||
let sortMenuOpen = false;
|
let sortMenuOpen = false;
|
||||||
|
let filterMenuOpen = false;
|
||||||
|
|
||||||
async function setGroup(projectTag: ProjectTag): Promise<boolean> {
|
async function setGroup(projectTag: ProjectTag): Promise<boolean> {
|
||||||
const view = get(currentView);
|
const view = get(currentView);
|
||||||
|
@ -54,7 +56,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class:disabled={true}>Sub-group</button>
|
<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>
|
<div>
|
||||||
<button on:click={() => (sortMenuOpen = !sortMenuOpen)} class:defined={$currentView?.sortTag}>
|
<button on:click={() => (sortMenuOpen = !sortMenuOpen)} class:defined={$currentView?.sortTag}>
|
||||||
Sort
|
Sort
|
||||||
|
|
|
@ -10,14 +10,14 @@ export default class Filter {
|
||||||
private _view: View;
|
private _view: View;
|
||||||
private _projectTag: ProjectTag;
|
private _projectTag: ProjectTag;
|
||||||
private _filterType: number;
|
private _filterType: number;
|
||||||
private _tagOption: TagOption;
|
private _tagOption: TagOption | null;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
id: number,
|
id: number,
|
||||||
view: View,
|
view: View,
|
||||||
projectTag: ProjectTag,
|
projectTag: ProjectTag,
|
||||||
filterType: number,
|
filterType: number,
|
||||||
tagOption: TagOption
|
tagOption: TagOption | null
|
||||||
) {
|
) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
this._view = view;
|
this._view = view;
|
||||||
|
@ -42,7 +42,7 @@ export default class Filter {
|
||||||
return this._filterType;
|
return this._filterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tagOption(): TagOption {
|
get tagOption(): TagOption | null {
|
||||||
return this._tagOption;
|
return this._tagOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,9 +50,9 @@ export default class Filter {
|
||||||
view: View,
|
view: View,
|
||||||
projectTag: ProjectTag,
|
projectTag: ProjectTag,
|
||||||
filterType: number,
|
filterType: number,
|
||||||
tagOption: TagOption
|
tagOption: TagOption | null
|
||||||
): Promise<Filter | 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;
|
if (!id) return null;
|
||||||
|
|
||||||
return new Filter(id, view, projectTag, filterType, tagOption);
|
return new Filter(id, view, projectTag, filterType, tagOption);
|
||||||
|
@ -62,6 +62,54 @@ export default class Filter {
|
||||||
return await filtersApi.delete(this.id);
|
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): Filter[];
|
||||||
static parseAll(json: any, view: View | null): 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);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,10 @@ export default class View {
|
||||||
return this._sortDirection;
|
return this._sortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filters(): Filter[] {
|
||||||
|
return this._filters;
|
||||||
|
}
|
||||||
|
|
||||||
static fromId(id: number): View | null {
|
static fromId(id: number): View | null {
|
||||||
for (const view of get(views)) {
|
for (const view of get(views)) {
|
||||||
if (view.id === id) {
|
if (view.id === id) {
|
||||||
|
@ -152,13 +156,17 @@ export default class View {
|
||||||
return true;
|
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);
|
const filter = await Filter.create(this, projectTag, filterType, option);
|
||||||
if (!filter) return false;
|
if (!filter) return null;
|
||||||
|
|
||||||
this._filters = [...this._filters, filter];
|
this._filters = [...this._filters, filter];
|
||||||
|
|
||||||
return true;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFilter(filter: Filter): Promise<boolean> {
|
async removeFilter(filter: Filter): Promise<boolean> {
|
||||||
|
|
Loading…
Reference in New Issue