Compare commits
6 Commits
b6e36fdf00
...
9bb9041580
Author | SHA1 | Date |
---|---|---|
Brieuc Dubois | 9bb9041580 | |
Brieuc Dubois | 2092125fc5 | |
Brieuc Dubois | ca3811a48b | |
Brieuc Dubois | c725d5154e | |
Brieuc Dubois | bb479c45ef | |
Brieuc Dubois | 7404aed139 |
|
@ -0,0 +1,76 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "git.bhasher.com/bhasher/focus/backend/types"
|
||||||
|
|
||||||
|
func CreateFilter(filter types.Filter) (int, error) {
|
||||||
|
res, err := db.Exec("INSERT INTO filters (view_id, tag_id, filter_type, option_id) VALUES (?, ?, ?, ?)", filter.ViewID, filter.TagID, filter.FilterType, filter.OptionID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetViewFilters(viewID int) ([]types.Filter, error) {
|
||||||
|
rows, err := db.Query("SELECT * FROM filters WHERE view_id = ?", viewID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var filters []types.Filter
|
||||||
|
for rows.Next() {
|
||||||
|
var f types.Filter
|
||||||
|
if err := rows.Scan(&f.ID, &f.ViewID, &f.TagID, &f.FilterType, &f.OptionID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = append(filters, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFilter(id int) (*types.Filter, error) {
|
||||||
|
rows, err := db.Query("SELECT * FROM filters WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var f types.Filter
|
||||||
|
rows.Scan(&f.ID, &f.ViewID, &f.TagID, &f.FilterType, &f.OptionID)
|
||||||
|
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateFilter(f types.Filter) (int64, error) {
|
||||||
|
res, err := db.Exec("UPDATE filters SET view_id = ?, tag_id = ?, filter_type = ?, option_id = ? WHERE id = ?", f.ViewID, f.TagID, f.FilterType, f.OptionID, f.ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFilter(id int) (int64, error) {
|
||||||
|
res, err := db.Exec("DELETE FROM filters WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
|
@ -67,6 +67,17 @@ func InitDB(driver string, connStr string) error {
|
||||||
FOREIGN KEY(secondary_tag_id) REFERENCES tags(id)
|
FOREIGN KEY(secondary_tag_id) REFERENCES tags(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS filters (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
view_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
filter_type INTEGER NOT NULL,
|
||||||
|
option_id INTEGER,
|
||||||
|
FOREIGN KEY(view_id) REFERENCES views(id),
|
||||||
|
FOREIGN KEY(tag_id) REFERENCES tags(id),
|
||||||
|
FOREIGN KEY(option_id) REFERENCES tagsoptions(id)
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO schema_version (version)
|
INSERT INTO schema_version (version)
|
||||||
SELECT ? WHERE NOT EXISTS (SELECT 1 FROM schema_version);
|
SELECT ? WHERE NOT EXISTS (SELECT 1 FROM schema_version);
|
||||||
`, DB_VERSION)
|
`, DB_VERSION)
|
||||||
|
|
|
@ -16,20 +16,26 @@ func CreateView(v types.View) (int, error) {
|
||||||
return int(id), nil
|
return int(id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProjectViews(projectID int) ([]types.View, error) {
|
func GetProjectViews(projectID int) ([]types.FullView, error) {
|
||||||
rows, err := db.Query("SELECT * FROM views WHERE project_id = ?", projectID)
|
rows, err := db.Query("SELECT * FROM views WHERE project_id = ?", projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var views []types.View
|
var views []types.FullView
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var v types.View
|
var v types.FullView
|
||||||
if err := rows.Scan(&v.ID, &v.ProjectID, &v.PrimaryTagID, &v.SecondaryTagID, &v.Title, &v.SortTagID, &v.SortDirection); err != nil {
|
if err := rows.Scan(&v.ID, &v.ProjectID, &v.PrimaryTagID, &v.SecondaryTagID, &v.Title, &v.SortTagID, &v.SortDirection); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters, err := GetViewFilters(v.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Filters = filters
|
||||||
|
|
||||||
views = append(views, v)
|
views = append(views, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +46,7 @@ func GetProjectViews(projectID int) ([]types.View, error) {
|
||||||
return views, nil
|
return views, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetView(id int) (*types.View, error) {
|
func GetView(id int) (*types.FullView, error) {
|
||||||
rows, err := db.Query("SELECT * FROM views WHERE id = ?", id)
|
rows, err := db.Query("SELECT * FROM views WHERE id = ?", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -51,9 +57,15 @@ func GetView(id int) (*types.View, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var v types.View
|
var v types.FullView
|
||||||
rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.SortTagID, v.SortDirection)
|
rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.SortTagID, v.SortDirection)
|
||||||
|
|
||||||
|
filters, err := GetViewFilters(id)
|
||||||
|
if(err != nil) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Filters = filters
|
||||||
|
|
||||||
return &v, nil
|
return &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,3 +86,13 @@ func DeleteView(id int) (int64, error) {
|
||||||
|
|
||||||
return res.RowsAffected()
|
return res.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExistView(id int) (bool, error) {
|
||||||
|
var count int
|
||||||
|
err := db.QueryRow("SELECT COUNT(*) FROM views WHERE id = ?", id).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.bhasher.com/bhasher/focus/backend/db"
|
||||||
|
"git.bhasher.com/bhasher/focus/backend/types"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func filtersRouter(router fiber.Router) error {
|
||||||
|
router.Post("/", CreateFilter)
|
||||||
|
router.Get("/:id", GetFilter)
|
||||||
|
router.Put("/:id", UpdateFilter)
|
||||||
|
router.Delete("/:id", DeleteFilter)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFilter(c *fiber.Ctx) error {
|
||||||
|
filter := types.Filter{}
|
||||||
|
if err := c.BodyParser(&filter); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Cannot parse request",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exist, err := db.ExistView(filter.ViewID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Error finding view",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "View not found"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := db.CreateFilter(filter)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Cannot create filter",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFilter(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid filter ID"})
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := db.GetFilter(id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Cannot retrieve filter",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter == nil {
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateFilter(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid filter ID"})
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := types.Filter{ID: id}
|
||||||
|
if err := c.BodyParser(&filter); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Cannot parse request",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
exist, err := db.ExistView(filter.ViewID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Error finding view",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "View not found"})
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := db.UpdateFilter(filter)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Cannot update filter",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFilter(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid filter ID"})
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := db.DeleteFilter(id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Cannot delete filter",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ func v1Router(router fiber.Router) error {
|
||||||
cardsRouter(router.Group("/cards"))
|
cardsRouter(router.Group("/cards"))
|
||||||
tagsRouter(router.Group("/tags"))
|
tagsRouter(router.Group("/tags"))
|
||||||
viewsRouter(router.Group("/views"))
|
viewsRouter(router.Group("/views"))
|
||||||
|
filtersRouter(router.Group("/filters"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ func viewsRouter(router fiber.Router) error {
|
||||||
router.Get("/:id", GetView)
|
router.Get("/:id", GetView)
|
||||||
router.Put("/:id", UpdateView)
|
router.Put("/:id", UpdateView)
|
||||||
router.Delete("/:id", DeleteView)
|
router.Delete("/:id", DeleteView)
|
||||||
|
router.Get("/:id/filters", GetView)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,3 +108,20 @@ func DeleteView(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetViewFilters(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"})
|
||||||
|
}
|
||||||
|
|
||||||
|
filters, err := db.GetViewFilters(id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Cannot retrieve view filters",
|
||||||
|
"trace": fmt.Sprint(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(filters)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
ViewID int `json:"view_id"`
|
||||||
|
TagID int `json:"tag_id"`
|
||||||
|
FilterType int `json:"filter_type"`
|
||||||
|
OptionID int `json:"option_id"`
|
||||||
|
}
|
|
@ -9,3 +9,14 @@ type View struct {
|
||||||
SortTagID *int `json:"sort_tag_id"`
|
SortTagID *int `json:"sort_tag_id"`
|
||||||
SortDirection *int `json:"sort_direction"`
|
SortDirection *int `json:"sort_direction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FullView struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
ProjectID int `json:"project_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
PrimaryTagID *int `json:"primary_tag_id"`
|
||||||
|
SecondaryTagID *int `json:"secondary_tag_id"`
|
||||||
|
SortTagID *int `json:"sort_tag_id"`
|
||||||
|
SortDirection *int `json:"sort_direction"`
|
||||||
|
Filters []Filter `json:"filters"`
|
||||||
|
}
|
|
@ -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 | null
|
||||||
|
): Promise<number | null> {
|
||||||
|
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 | null
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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_
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
|
import Filter from '$lib/types/Filter';
|
||||||
import type Project from '$lib/types/Project';
|
import type Project from '$lib/types/Project';
|
||||||
import type View from '$lib/types/View';
|
|
||||||
import api, { processError } from '$lib/utils/api';
|
import api, { processError } from '$lib/utils/api';
|
||||||
import status from '$lib/utils/status';
|
import status from '$lib/utils/status';
|
||||||
|
|
||||||
|
@ -53,8 +53,20 @@ async function delete_(viewId: number): Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFilters(viewId: number): Promise<Filter[]> {
|
||||||
|
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 {
|
export default {
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
delete: delete_
|
delete: delete_,
|
||||||
|
getFilters
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import currentView from '$lib/stores/currentView';
|
||||||
|
import type Filter from '$lib/types/Filter';
|
||||||
|
import FilterMenuItem from './FilterMenuItem.svelte';
|
||||||
|
import Menu from './Menu.svelte';
|
||||||
|
|
||||||
|
export let isOpen = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $currentView}
|
||||||
|
<Menu bind:isOpen>
|
||||||
|
{#each $currentView.filters as filter}
|
||||||
|
<FilterMenuItem {filter} />
|
||||||
|
{/each}
|
||||||
|
<FilterMenuItem />
|
||||||
|
</Menu>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
</style>
|
|
@ -0,0 +1,190 @@
|
||||||
|
<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 TrashIcon from '../icons/TrashIcon.svelte';
|
||||||
|
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) {
|
||||||
|
await $currentView?.addFilter(projectTag, 0, null);
|
||||||
|
currentView.reload();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFilter() {
|
||||||
|
if (!filter) return;
|
||||||
|
|
||||||
|
const res = await $currentView?.removeFilter(filter);
|
||||||
|
if (!res) return;
|
||||||
|
currentView.reload();
|
||||||
|
}
|
||||||
|
</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}
|
||||||
|
{#if filter.filterType === 0}
|
||||||
|
is
|
||||||
|
{:else if filter.filterType === 1}
|
||||||
|
is not
|
||||||
|
{/if}
|
||||||
|
{/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>
|
||||||
|
{#if filter}
|
||||||
|
<div
|
||||||
|
class="delete"
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
on:click={() => deleteFilter()}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter') deleteFilter();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon size={20} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part,
|
||||||
|
.delete {
|
||||||
|
height: 30px;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fff2;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 100px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 2px 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,9 +3,10 @@
|
||||||
import CardComponent from '../card/Card.svelte';
|
import CardComponent from '../card/Card.svelte';
|
||||||
import AddIcon from '../icons/AddIcon.svelte';
|
import AddIcon from '../icons/AddIcon.svelte';
|
||||||
import type TagOption from '$lib/types/TagOption';
|
import type TagOption from '$lib/types/TagOption';
|
||||||
import type ProjectTag from '$lib/types/ProjectTag';
|
import ProjectTag, { projectTags } from '$lib/types/ProjectTag';
|
||||||
import type Project from '$lib/types/Project';
|
import type Project from '$lib/types/Project';
|
||||||
import currentDraggedCard from '$lib/stores/currentDraggedCard';
|
import currentDraggedCard from '$lib/stores/currentDraggedCard';
|
||||||
|
import currentView from '$lib/stores/currentView';
|
||||||
|
|
||||||
export let project: Project;
|
export let project: Project;
|
||||||
export let option: TagOption | null = null;
|
export let option: TagOption | null = null;
|
||||||
|
@ -43,10 +44,22 @@
|
||||||
const card = await Card.create(project);
|
const card = await Card.create(project);
|
||||||
|
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
if (!primaryTag) return;
|
|
||||||
if (!option) return;
|
|
||||||
|
|
||||||
|
if ($currentView?.filters && $currentView.filters.length > 0) {
|
||||||
|
for (const projectTag of $projectTags) {
|
||||||
|
for (const filter of $currentView.filters) {
|
||||||
|
if (projectTag !== filter.projectTag) continue;
|
||||||
|
if (!filter.tagOption) continue;
|
||||||
|
if (filter.filterType !== 0) continue;
|
||||||
|
|
||||||
|
if (await card.addTag(projectTag, filter.tagOption, null)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryTag && option) {
|
||||||
await card.addTag(primaryTag, option, null);
|
await card.addTag(primaryTag, option, null);
|
||||||
|
}
|
||||||
|
|
||||||
cards.reload();
|
cards.reload();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@
|
||||||
import GroupMenu from '$lib/components/menu/GroupMenu.svelte';
|
import GroupMenu from '$lib/components/menu/GroupMenu.svelte';
|
||||||
import SortMenu from '$lib/components/menu/SortMenu.svelte';
|
import SortMenu from '$lib/components/menu/SortMenu.svelte';
|
||||||
import currentView from '$lib/stores/currentView';
|
import currentView from '$lib/stores/currentView';
|
||||||
import Card from '$lib/types/Card';
|
import Card, { cards } from '$lib/types/Card';
|
||||||
import type Project from '$lib/types/Project';
|
import type Project from '$lib/types/Project';
|
||||||
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);
|
||||||
|
@ -33,6 +35,26 @@
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addCard() {
|
||||||
|
const card = await Card.create(project);
|
||||||
|
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
if ($currentView?.filters && $currentView.filters.length > 0) {
|
||||||
|
for (const projectTag of $projectTags) {
|
||||||
|
for (const filter of $currentView.filters) {
|
||||||
|
if (projectTag !== filter.projectTag) continue;
|
||||||
|
if (!filter.tagOption) continue;
|
||||||
|
if (filter.filterType !== 0) continue;
|
||||||
|
|
||||||
|
if (await card.addTag(projectTag, filter.tagOption, null)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.reload();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
@ -54,7 +76,15 @@
|
||||||
/>
|
/>
|
||||||
</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)}
|
||||||
|
class:defined={$currentView?.filters && $currentView?.filters.length > 0}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
<FilterMenu bind:isOpen={filterMenuOpen} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button on:click={() => (sortMenuOpen = !sortMenuOpen)} class:defined={$currentView?.sortTag}>
|
<button on:click={() => (sortMenuOpen = !sortMenuOpen)} class:defined={$currentView?.sortTag}>
|
||||||
Sort
|
Sort
|
||||||
|
@ -70,7 +100,7 @@
|
||||||
currentDirection={$currentView?.sortDirection || null}
|
currentDirection={$currentView?.sortDirection || null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button id="newButton" on:click={async () => Card.create(project)}>New</button>
|
<button id="newButton" on:click={addCard}>New</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,103 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import currentView from '$lib/stores/currentView';
|
import currentView from '$lib/stores/currentView';
|
||||||
import { cards } from '$lib/types/Card';
|
import Card, { cards } from '$lib/types/Card';
|
||||||
|
import type Filter from '$lib/types/Filter';
|
||||||
import type Project from '$lib/types/Project';
|
import type Project from '$lib/types/Project';
|
||||||
|
import type ProjectTag from '$lib/types/ProjectTag';
|
||||||
|
import { projectTags } from '$lib/types/ProjectTag';
|
||||||
|
import type TagOption from '$lib/types/TagOption';
|
||||||
|
import type View from '$lib/types/View';
|
||||||
import Column from './Column.svelte';
|
import Column from './Column.svelte';
|
||||||
import Header from './Header.svelte';
|
import Header from './Header.svelte';
|
||||||
|
|
||||||
export let project: Project;
|
export let project: Project;
|
||||||
|
|
||||||
$: allCards = $cards;
|
function cardComparator(a: Card, b: Card, sortTag: ProjectTag | null, sortDirection: number) {
|
||||||
|
if (!sortTag) return 0;
|
||||||
|
|
||||||
|
const aTag = a.cardTags.find((t) => t.projectTag === sortTag);
|
||||||
|
const bTag = b.cardTags.find((t) => t.projectTag === sortTag);
|
||||||
|
|
||||||
|
if (!aTag) return -sortDirection;
|
||||||
|
if (!bTag) return sortDirection;
|
||||||
|
|
||||||
|
const aValue = aTag.value || aTag.option?.value || '';
|
||||||
|
const bValue = bTag.value || bTag.option?.value || '';
|
||||||
|
|
||||||
|
return aValue < bValue ? sortDirection : -sortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passFilters(card: Card, filters: Filter[]): boolean {
|
||||||
|
for (const projectTag of $projectTags) {
|
||||||
|
let is: TagOption[] = [];
|
||||||
|
|
||||||
|
const cardTag = card.cardTags.find((t) => t.projectTag === projectTag);
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (projectTag !== filter.projectTag) continue;
|
||||||
|
if (!filter.tagOption) continue;
|
||||||
|
|
||||||
|
if (filter.filterType === 0) {
|
||||||
|
is.push(filter.tagOption);
|
||||||
|
} else if (filter.filterType === 1) {
|
||||||
|
if (cardTag?.option === filter.tagOption) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is.length > 0) {
|
||||||
|
if (!cardTag) return false;
|
||||||
|
if (!is.some((o) => o === cardTag?.option)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractColumnCards(view: View | null, cards: Card[], tagOption: TagOption | null) {
|
||||||
|
if (!view) return cards;
|
||||||
|
|
||||||
|
const filteredCards = cards.filter((c) => passFilters(c, view.filters));
|
||||||
|
|
||||||
|
const primaryTag = view.primaryTag;
|
||||||
|
|
||||||
|
if (!primaryTag) return filteredCards;
|
||||||
|
|
||||||
|
if (!tagOption) {
|
||||||
|
return filteredCards.filter((c) => !c.cardTags.map((t) => t.projectTag).includes(primaryTag));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightOptionCards = filteredCards.filter((c) => {
|
||||||
|
const tag = c.cardTags.find((t) => t.projectTag === primaryTag);
|
||||||
|
if (!tag) return false;
|
||||||
|
return tag.option?.id === tagOption.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedCards = rightOptionCards.sort((a, b) =>
|
||||||
|
cardComparator(a, b, view.sortTag, $currentView?.sortDirection || 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedCards;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if project}
|
{#if project}
|
||||||
<section>
|
<section>
|
||||||
{#if $currentView}
|
{#if $currentView}
|
||||||
<Header {project} />
|
<Header {project} />
|
||||||
{#if $cards && allCards}
|
{#if $cards}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#if $currentView.primaryTag}
|
{#if $currentView.primaryTag}
|
||||||
{#each $currentView.primaryTag.options as option}
|
{#each $currentView.primaryTag.options as option}
|
||||||
<Column
|
<Column
|
||||||
{option}
|
{option}
|
||||||
primaryTag={$currentView.primaryTag}
|
primaryTag={$currentView.primaryTag}
|
||||||
columnCards={allCards
|
columnCards={extractColumnCards($currentView, $cards, option)}
|
||||||
.filter((c) => c.cardTags.map((t) => t.option).includes(option))
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (!$currentView?.sortTag) return 0;
|
|
||||||
const aTag = a.cardTags.find((t) => t.projectTag === $currentView?.sortTag);
|
|
||||||
const bTag = b.cardTags.find((t) => t.projectTag === $currentView?.sortTag);
|
|
||||||
|
|
||||||
if (!aTag) return -($currentView?.sortDirection || 1);
|
|
||||||
if (!bTag) return $currentView?.sortDirection || 1;
|
|
||||||
|
|
||||||
const aValue = aTag.value || aTag.option?.value || '';
|
|
||||||
const bValue = bTag.value || bTag.option?.value || '';
|
|
||||||
|
|
||||||
return aValue < bValue
|
|
||||||
? $currentView?.sortDirection || 1
|
|
||||||
: -($currentView?.sortDirection || 1);
|
|
||||||
})}
|
|
||||||
{project}
|
{project}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<Column
|
<Column
|
||||||
primaryTag={$currentView.primaryTag}
|
primaryTag={$currentView.primaryTag}
|
||||||
columnCards={$currentView.primaryTag != null
|
columnCards={extractColumnCards($currentView, $cards, null)}
|
||||||
? (() => {
|
|
||||||
const primaryTag = $currentView.primaryTag;
|
|
||||||
return allCards.filter(
|
|
||||||
(c) => !c.cardTags.map((t) => t.projectTag).includes(primaryTag)
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
: allCards}
|
|
||||||
{project}
|
{project}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
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 | null;
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
id: number,
|
||||||
|
view: View,
|
||||||
|
projectTag: ProjectTag,
|
||||||
|
filterType: number,
|
||||||
|
tagOption: TagOption | null
|
||||||
|
) {
|
||||||
|
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 | null {
|
||||||
|
return this._tagOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(
|
||||||
|
view: View,
|
||||||
|
projectTag: ProjectTag,
|
||||||
|
filterType: number,
|
||||||
|
tagOption: TagOption | null
|
||||||
|
): Promise<Filter | null> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<boolean> {
|
||||||
|
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[];
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new Filter(json.id, view, projectTag, json.filter_type, tagOption || null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import { writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import Project from './Project';
|
import Project from './Project';
|
||||||
import ProjectTag from './ProjectTag';
|
import ProjectTag from './ProjectTag';
|
||||||
import viewsApi from '$lib/api/viewsApi';
|
import viewsApi from '$lib/api/viewsApi';
|
||||||
import { toastAlert } from '$lib/utils/toasts';
|
import { toastAlert } from '$lib/utils/toasts';
|
||||||
|
import Filter from './Filter';
|
||||||
|
import type TagOption from './TagOption';
|
||||||
|
|
||||||
export const views = writable([] as View[]);
|
export const views = writable([] as View[]);
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ export default class View {
|
||||||
private _title: string;
|
private _title: string;
|
||||||
private _sortTag: ProjectTag | null;
|
private _sortTag: ProjectTag | null;
|
||||||
private _sortDirection: number | null;
|
private _sortDirection: number | null;
|
||||||
|
private _filters: Filter[];
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -22,7 +25,8 @@ export default class View {
|
||||||
secondaryTag: ProjectTag | null,
|
secondaryTag: ProjectTag | null,
|
||||||
title: string,
|
title: string,
|
||||||
sortTag: ProjectTag | null,
|
sortTag: ProjectTag | null,
|
||||||
sortDirection: number | null
|
sortDirection: number | null,
|
||||||
|
filters: Filter[]
|
||||||
) {
|
) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
this._project = project;
|
this._project = project;
|
||||||
|
@ -31,6 +35,7 @@ export default class View {
|
||||||
this._title = title;
|
this._title = title;
|
||||||
this._sortTag = sortTag;
|
this._sortTag = sortTag;
|
||||||
this._sortDirection = sortDirection;
|
this._sortDirection = sortDirection;
|
||||||
|
this._filters = filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id(): number {
|
get id(): number {
|
||||||
|
@ -61,6 +66,20 @@ export default class View {
|
||||||
return this._sortDirection;
|
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) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async setPrimaryTag(projectTag: ProjectTag): Promise<boolean> {
|
async setPrimaryTag(projectTag: ProjectTag): Promise<boolean> {
|
||||||
const response = await viewsApi.update(
|
const response = await viewsApi.update(
|
||||||
this.id,
|
this.id,
|
||||||
|
@ -103,7 +122,7 @@ export default class View {
|
||||||
|
|
||||||
if (!id) return null;
|
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]);
|
views.update((views) => [...views, view]);
|
||||||
|
|
||||||
|
@ -137,6 +156,27 @@ export default class View {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addFilter(
|
||||||
|
projectTag: ProjectTag,
|
||||||
|
filterType: number,
|
||||||
|
option: TagOption | null
|
||||||
|
): Promise<Filter | null> {
|
||||||
|
const filter = await Filter.create(this, projectTag, filterType, option);
|
||||||
|
if (!filter) return null;
|
||||||
|
|
||||||
|
this._filters = [...this._filters, filter];
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFilter(filter: Filter): Promise<boolean> {
|
||||||
|
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): View | null;
|
||||||
static parse(json: any, project: Project | null | undefined): View | null;
|
static parse(json: any, project: Project | null | undefined): View | null;
|
||||||
|
|
||||||
|
@ -163,9 +203,12 @@ export default class View {
|
||||||
secondaryTag,
|
secondaryTag,
|
||||||
json.title,
|
json.title,
|
||||||
sortTag,
|
sortTag,
|
||||||
json.sort_direction
|
json.sort_direction,
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
view._filters = Filter.parseAll(json.filters, view);
|
||||||
|
|
||||||
views.update((views) => {
|
views.update((views) => {
|
||||||
if (!views.find((view) => view.id === json.id)) {
|
if (!views.find((view) => view.id === json.id)) {
|
||||||
return [...views, view];
|
return [...views, view];
|
||||||
|
|
Loading…
Reference in New Issue