diff --git a/frontend/src/lib/api/filtersApi.ts b/frontend/src/lib/api/filtersApi.ts new file mode 100644 index 0000000..c26e040 --- /dev/null +++ b/frontend/src/lib/api/filtersApi.ts @@ -0,0 +1,62 @@ +import api, { processError } from '$lib/utils/api'; +import status from '$lib/utils/status'; + +async function create( + viewId: number, + projectTagId: number, + filterType: number, + tagOptionId: number +): Promise { + const response = await api.post(`/v1/filters`, { + view_id: viewId, + tag_id: projectTagId, + filter_type: filterType, + option_id: tagOptionId + }); + + if (response.status !== status.Created) { + processError(response, 'Failed to create filter'); + return null; + } + + return response.data.id; +} + +async function update( + filterId: number, + viewId: number, + projectTagId: number, + filterType: number, + tagOptionId: number +): Promise { + const response = await api.put(`/v1/filters/${filterId}`, { + view_id: viewId, + tag_id: projectTagId, + filter_type: filterType, + option_id: tagOptionId + }); + + if (response.status !== status.NoContent) { + processError(response, 'Failed to update filter'); + return false; + } + + return true; +} + +async function delete_(filterId: number): Promise { + const response = await api.delete(`/v1/filters/${filterId}`); + + if (response.status !== status.NoContent) { + processError(response, 'Failed to delete filter'); + return false; + } + + return true; +} + +export default { + create, + update, + delete: delete_ +}; diff --git a/frontend/src/lib/api/viewsApi.ts b/frontend/src/lib/api/viewsApi.ts index 0b0b359..6371c56 100644 --- a/frontend/src/lib/api/viewsApi.ts +++ b/frontend/src/lib/api/viewsApi.ts @@ -1,5 +1,5 @@ +import Filter from '$lib/types/Filter'; import type Project from '$lib/types/Project'; -import type View from '$lib/types/View'; import api, { processError } from '$lib/utils/api'; import status from '$lib/utils/status'; @@ -53,8 +53,20 @@ async function delete_(viewId: number): Promise { return true; } +async function getFilters(viewId: number): Promise { + const response = await api.get(`/v1/views/${viewId}/filters`); + + if (response.status !== status.OK) { + processError(response, 'Failed to get view filters'); + return []; + } + + return Filter.parseAll(response.data); +} + export default { create, update, - delete: delete_ + delete: delete_, + getFilters }; diff --git a/frontend/src/lib/types/Filter.ts b/frontend/src/lib/types/Filter.ts new file mode 100644 index 0000000..01bae67 --- /dev/null +++ b/frontend/src/lib/types/Filter.ts @@ -0,0 +1,110 @@ +import { writable } from 'svelte/store'; +import View from './View'; +import ProjectTag from './ProjectTag'; +import TagOption from './TagOption'; +import { toastAlert } from '$lib/utils/toasts'; +import filtersApi from '$lib/api/filtersApi'; + +export default class Filter { + private _id: number; + private _view: View; + private _projectTag: ProjectTag; + private _filterType: number; + private _tagOption: TagOption; + + private constructor( + id: number, + view: View, + projectTag: ProjectTag, + filterType: number, + tagOption: TagOption + ) { + this._id = id; + this._view = view; + this._projectTag = projectTag; + this._filterType = filterType; + this._tagOption = tagOption; + } + + get id(): number { + return this._id; + } + + get view(): View { + return this._view; + } + + get projectTag(): ProjectTag { + return this._projectTag; + } + + get filterType(): number { + return this._filterType; + } + + get tagOption(): TagOption { + return this._tagOption; + } + + static async create( + view: View, + projectTag: ProjectTag, + filterType: number, + tagOption: TagOption + ): Promise { + const id = await filtersApi.create(view.id, projectTag.id, filterType, tagOption.id); + if (!id) return null; + + return new Filter(id, view, projectTag, filterType, tagOption); + } + + async delete(): Promise { + return await filtersApi.delete(this.id); + } + + static parseAll(json: any): Filter[]; + static parseAll(json: any, view: View | null): Filter[]; + + static parseAll(json: any[], view?: View | null): Filter[] { + if (!json) return []; + + const filters: Filter[] = []; + + for (const filter of json) { + const parsed = Filter.parse(filter, view); + if (parsed) filters.push(parsed); + } + + return filters; + } + + static parse(json: any): Filter | null; + static parse(json: any, view: View | null | undefined): Filter | null; + + static parse(json: any, view?: View | null | undefined): Filter | null { + if (!json) { + toastAlert('Failed to parse filter: json is null'); + return null; + } + + if (!view) view = View.fromId(json.view_id); + if (!view) { + toastAlert('Failed to parse filter: view not found'); + return null; + } + + const projectTag = ProjectTag.fromId(json.tag_id); + if (!projectTag) { + toastAlert('Failed to parse filter: projectTag not found'); + return null; + } + + 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); + } +} diff --git a/frontend/src/lib/types/View.ts b/frontend/src/lib/types/View.ts index 70e1d60..8d0c8c5 100644 --- a/frontend/src/lib/types/View.ts +++ b/frontend/src/lib/types/View.ts @@ -1,8 +1,10 @@ -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; import Project from './Project'; import ProjectTag from './ProjectTag'; import viewsApi from '$lib/api/viewsApi'; import { toastAlert } from '$lib/utils/toasts'; +import Filter from './Filter'; +import type TagOption from './TagOption'; export const views = writable([] as View[]); @@ -14,6 +16,7 @@ export default class View { private _title: string; private _sortTag: ProjectTag | null; private _sortDirection: number | null; + private _filters: Filter[]; private constructor( id: number, @@ -22,7 +25,8 @@ export default class View { secondaryTag: ProjectTag | null, title: string, sortTag: ProjectTag | null, - sortDirection: number | null + sortDirection: number | null, + filters: Filter[] ) { this._id = id; this._project = project; @@ -31,6 +35,7 @@ export default class View { this._title = title; this._sortTag = sortTag; this._sortDirection = sortDirection; + this._filters = filters; } get id(): number { @@ -61,6 +66,16 @@ export default class View { return this._sortDirection; } + static fromId(id: number): View | null { + for (const view of get(views)) { + if (view.id === id) { + return view; + } + } + + return null; + } + async setPrimaryTag(projectTag: ProjectTag): Promise { const response = await viewsApi.update( this.id, @@ -103,7 +118,7 @@ export default class View { if (!id) return null; - const view = new View(id, project, null, null, 'New view', null, null); + const view = new View(id, project, null, null, 'New view', null, null, []); views.update((views) => [...views, view]); @@ -137,6 +152,23 @@ export default class View { return true; } + async addFilter(projectTag: ProjectTag, filterType: number, option: TagOption): Promise { + const filter = await Filter.create(this, projectTag, filterType, option); + if (!filter) return false; + + this._filters = [...this._filters, filter]; + + return true; + } + + async removeFilter(filter: Filter): Promise { + if (!(await filter.delete())) return false; + + this._filters = this._filters.filter((f) => f.id !== filter.id); + + return true; + } + static parse(json: any): View | null; static parse(json: any, project: Project | null | undefined): View | null; @@ -163,9 +195,12 @@ export default class View { secondaryTag, json.title, sortTag, - json.sort_direction + json.sort_direction, + [] ); + view._filters = Filter.parseAll(json.filters, view); + views.update((views) => { if (!views.find((view) => view.id === json.id)) { return [...views, view];