add project linking.
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-04-03 16:30:43 +02:00
parent 2c5d87134d
commit 2a6c04893d
17 changed files with 365 additions and 46 deletions
+60
View File
@@ -92,6 +92,15 @@ func Task(g *gin.RouterGroup, db database.Database) {
return nil, slerrors.NotFound("Destination project") return nil, slerrors.NotFound("Destination project")
} }
// Delete any links that would have been ran over.
err = db.Tasks().DeleteLink(c.Request.Context(), models.TaskLink{
TaskID: task.ID,
ProjectID: project.ID,
})
if err != nil {
return nil, slerrors.BadRequest(err.Error())
}
task.ProjectID = project.ID task.ProjectID = project.ID
} }
@@ -111,6 +120,57 @@ func Task(g *gin.RouterGroup, db database.Database) {
return task, nil return task, nil
})) }))
g.PUT("/:id/link/:project_id", handler("taskLink", func(c *gin.Context) (interface{}, error) {
task, err := l.FindTask(c.Request.Context(), c.Param("id"))
if err != nil {
return nil, err
}
project, err := l.FindProject(c.Request.Context(), c.Param("project_id"))
if err != nil {
return nil, err
}
if task.ProjectID == project.ID {
return nil, slerrors.BadRequest("You cannot link a task within its own project")
}
err = db.Tasks().CreateLink(c.Request.Context(), models.TaskLink{
TaskID: task.ID,
ProjectID: project.ID,
})
return &models.TaskLink{
TaskID: task.ID,
ProjectID: project.ID,
}, nil
}))
g.DELETE("/:id/link/:project_id", handler("taskLink", func(c *gin.Context) (interface{}, error) {
task, err := l.FindTask(c.Request.Context(), c.Param("id"))
if err != nil {
return nil, err
}
project, err := l.FindProject(c.Request.Context(), c.Param("project_id"))
if err != nil {
return nil, err
}
err = db.Tasks().DeleteLink(c.Request.Context(), models.TaskLink{
TaskID: task.ID,
ProjectID: project.ID,
})
if err != nil {
return nil, err
}
return &models.TaskLink{
TaskID: task.ID,
ProjectID: project.ID,
}, nil
}))
g.DELETE("/:id", handler("task", func(c *gin.Context) (interface{}, error) { g.DELETE("/:id", handler("task", func(c *gin.Context) (interface{}, error) {
task, err := l.FindTask(c.Request.Context(), c.Param("id")) task, err := l.FindTask(c.Request.Context(), c.Param("id"))
if err != nil { if err != nil {
+5
View File
@@ -105,5 +105,10 @@ func (r *projectRepository) Delete(ctx context.Context, project models.Project)
return err return err
} }
_, err = r.db.ExecContext(ctx, `DELETE FROM task_link WHERE project_id=$1`, project.ID)
if err != nil && err != sql.ErrNoRows {
return err
}
return nil return nil
} }
+89 -9
View File
@@ -28,7 +28,17 @@ func (r *taskRepository) Find(ctx context.Context, id string) (*models.Task, err
} }
func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]*models.Task, error) { func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]*models.Task, error) {
sq := squirrel.Select("task.*", "p.icon").From("task").PlaceholderFormat(squirrel.Dollar) tasks, _, err := r.ListWithLinks(ctx, filter)
return tasks, err
}
func (r *taskRepository) ListWithLinks(ctx context.Context, filter models.TaskFilter) ([]*models.Task, []*models.TaskLink, error) {
type tasksWithLinks struct {
models.Task
LinkedProjectID *string `db:"tl_project_id"`
}
sq := squirrel.Select("task.*", "tl.project_id as tl_project_id", "p.icon").From("task").PlaceholderFormat(squirrel.Dollar)
sq = sq.Where(squirrel.Eq{"task.user_id": filter.UserID}) sq = sq.Where(squirrel.Eq{"task.user_id": filter.UserID})
if filter.Active != nil { if filter.Active != nil {
sq = sq.Where(squirrel.Eq{"task.active": *filter.Active}) sq = sq.Where(squirrel.Eq{"task.active": *filter.Active})
@@ -47,7 +57,12 @@ func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]
sq = sq.Where(squirrel.Eq{"task.item_id": filter.ItemIDs}) sq = sq.Where(squirrel.Eq{"task.item_id": filter.ItemIDs})
} }
if filter.ProjectIDs != nil { if filter.ProjectIDs != nil {
sq = sq.Where(squirrel.Eq{"task.project_id": filter.ProjectIDs}) sq = sq.LeftJoin("task_link AS tl ON task.task_id = tl.task_id")
sq = sq.Where(squirrel.Or{
squirrel.Eq{"task.project_id": filter.ProjectIDs},
squirrel.Eq{"tl.project_id": filter.ProjectIDs},
})
} }
sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id") sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id")
@@ -55,19 +70,37 @@ func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]
query, args, err := sq.ToSql() query, args, err := sq.ToSql()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
res := make([]*models.Task, 0, 8) rows := make([]tasksWithLinks, 0, 8)
err = r.db.SelectContext(ctx, &res, query, args...) err = r.db.SelectContext(ctx, &rows, query, args...)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return res, nil return []*models.Task{}, []*models.TaskLink{}, nil
} }
return nil, err return nil, nil, err
} }
return res, nil added := make(map[string]bool)
tasks := make([]*models.Task, 0, len(rows))
links := make([]*models.TaskLink, 0, len(rows))
for _, row := range rows {
if row.LinkedProjectID != nil {
links = append(links, &models.TaskLink{
TaskID: row.Task.ID,
ProjectID: *row.LinkedProjectID,
})
}
if !added[row.Task.ID] {
task := row.Task
tasks = append(tasks, &task)
added[row.Task.ID] = true
}
}
return tasks, links, nil
} }
func (r *taskRepository) Insert(ctx context.Context, task models.Task) error { func (r *taskRepository) Insert(ctx context.Context, task models.Task) error {
@@ -101,7 +134,7 @@ func (r *taskRepository) Update(ctx context.Context, task models.Task) error {
if err != nil { if err != nil {
return err return err
} }
_, err = r.db.NamedExecContext(ctx, `UPDATE log SET item_id = :item_id WHERE task_id=:task_id`, &task) _, err = r.db.NamedExecContext(ctx, `UPDATE log SET item_id = :item_id WHERE task_id=:task_id`, &task)
if err != nil { if err != nil {
return err return err
@@ -110,6 +143,48 @@ func (r *taskRepository) Update(ctx context.Context, task models.Task) error {
return nil return nil
} }
func (r *taskRepository) CreateLink(ctx context.Context, link models.TaskLink) error {
_, err := r.db.NamedExecContext(ctx, `
INSERT INTO task_link (project_id, task_id) VALUES (:project_id, :task_id)
ON CONFLICT DO NOTHING
`, &link)
return err
}
func (r *taskRepository) DeleteLink(ctx context.Context, link models.TaskLink) error {
_, err := r.db.NamedExecContext(ctx, `
DELETE FROM task_link WHERE task_id=:task_id AND project_id=:project_id
`, &link)
if err == sql.ErrNoRows {
err = slerrors.NotFound("Link")
}
return err
}
func (r *taskRepository) UnlinkTask(ctx context.Context, task models.Task) error {
_, err := r.db.ExecContext(ctx, `
DELETE FROM task_link WHERE task_id=$1;
`, task.ID)
if err == sql.ErrNoRows {
err = nil
}
return err
}
func (r *taskRepository) UnlinkProject(ctx context.Context, project models.Project) error {
_, err := r.db.ExecContext(ctx, `
DELETE FROM task_link WHERE task_id=$1;
`, project.ID)
if err == sql.ErrNoRows {
err = nil
}
return err
}
func (r *taskRepository) Delete(ctx context.Context, task models.Task) error { func (r *taskRepository) Delete(ctx context.Context, task models.Task) error {
_, err := r.db.ExecContext(ctx, `DELETE FROM task WHERE task_id=$1`, task.ID) _, err := r.db.ExecContext(ctx, `DELETE FROM task WHERE task_id=$1`, task.ID)
if err != nil { if err != nil {
@@ -119,5 +194,10 @@ func (r *taskRepository) Delete(ctx context.Context, task models.Task) error {
return err return err
} }
_, err = r.db.ExecContext(ctx, `DELETE FROM task_link WHERE task_id=$1`, task.ID)
if err != nil && err != sql.ErrNoRows {
return err
}
return nil return nil
} }
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE task_link (
project_id CHAR(16) NOT NULL,
task_id CHAR(16) NOT NULL,
PRIMARY KEY (project_id, task_id),
UNIQUE (task_id, project_id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE task_link;
-- +goose StatementEnd
+10
View File
@@ -67,6 +67,11 @@ type TaskUpdate struct {
ProjectID *string `json:"projectId"` ProjectID *string `json:"projectId"`
} }
type TaskLink struct {
TaskID string `json:"taskId" db:"task_id"`
ProjectID string `json:"projectId" db:"project_id"`
}
type TaskResult struct { type TaskResult struct {
Task Task
Item *Item `json:"item"` Item *Item `json:"item"`
@@ -87,7 +92,12 @@ type TaskFilter struct {
type TaskRepository interface { type TaskRepository interface {
Find(ctx context.Context, id string) (*Task, error) Find(ctx context.Context, id string) (*Task, error)
List(ctx context.Context, filter TaskFilter) ([]*Task, error) List(ctx context.Context, filter TaskFilter) ([]*Task, error)
ListWithLinks(ctx context.Context, filter TaskFilter) ([]*Task, []*TaskLink, error)
Insert(ctx context.Context, task Task) error Insert(ctx context.Context, task Task) error
Update(ctx context.Context, task Task) error Update(ctx context.Context, task Task) error
CreateLink(ctx context.Context, link TaskLink) error
DeleteLink(ctx context.Context, link TaskLink) error
UnlinkTask(ctx context.Context, task Task) error
UnlinkProject(ctx context.Context, project Project) error
Delete(ctx context.Context, task Task) error Delete(ctx context.Context, task Task) error
} }
+13 -2
View File
@@ -265,7 +265,7 @@ func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter)
projectIDs = append(projectIDs, project.ID) projectIDs = append(projectIDs, project.ID)
} }
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ tasks, links, err := l.DB.Tasks().ListWithLinks(ctx, models.TaskFilter{
UserID: auth.UserID(ctx), UserID: auth.UserID(ctx),
ProjectIDs: projectIDs, ProjectIDs: projectIDs,
}) })
@@ -301,7 +301,18 @@ func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter)
results[i].Tasks = make([]*models.TaskResult, 0, 16) results[i].Tasks = make([]*models.TaskResult, 0, 16)
for _, task := range tasks { for _, task := range tasks {
if task.ProjectID != project.ID { if task.ProjectID != project.ID {
continue foundLink := false
for _, link := range links {
if link.TaskID == task.ID && link.ProjectID == project.ID {
foundLink = true
break
}
}
if !foundLink {
continue
}
} }
taskResult := &models.TaskResult{ taskResult := &models.TaskResult{
+3
View File
@@ -19,6 +19,7 @@
import ProjectForm from "./forms/ProjectForm.svelte"; import ProjectForm from "./forms/ProjectForm.svelte";
import ItemForm from "./forms/ItemForm.svelte"; import ItemForm from "./forms/ItemForm.svelte";
import TaskForm from "./forms/TaskForm.svelte"; import TaskForm from "./forms/TaskForm.svelte";
import TaskLinkForm from "./forms/TaskLinkForm.svelte";
import LoginForm from "./forms/LoginForm.svelte"; import LoginForm from "./forms/LoginForm.svelte";
import ModalRoute from "./components/ModalRoute.svelte"; import ModalRoute from "./components/ModalRoute.svelte";
@@ -71,6 +72,8 @@
<ModalRoute name="goal.add"> <GoalForm creation /></ModalRoute> <ModalRoute name="goal.add"> <GoalForm creation /></ModalRoute>
<ModalRoute name="goal.edit"> <GoalForm /></ModalRoute> <ModalRoute name="goal.edit"> <GoalForm /></ModalRoute>
<ModalRoute name="goal.delete"> <GoalForm deletion /></ModalRoute> <ModalRoute name="goal.delete"> <GoalForm deletion /></ModalRoute>
<ModalRoute name="tasklink.add"> <TaskLinkForm creation /></ModalRoute>
<ModalRoute name="tasklink.delete"> <TaskLinkForm deletion /></ModalRoute>
{:else} {:else}
<LoginForm /> <LoginForm />
{/if} {/if}
+13 -1
View File
@@ -1,7 +1,7 @@
import { getJwt } from "./amplify"; import { getJwt } from "./amplify";
import type { GoalFilter, GoalInput, GoalResult, GoalUpdate } from "../models/goal"; import type { GoalFilter, GoalInput, GoalResult, GoalUpdate } from "../models/goal";
import type { ProjectFilter, ProjectInput, ProjectResult, ProjectUpdate } from "../models/project"; import type { ProjectFilter, ProjectInput, ProjectResult, ProjectUpdate } from "../models/project";
import type { TaskFilter, TaskInput, TaskResult, TaskUpdate } from "../models/task"; import type { TaskFilter, TaskInput, TaskLink, TaskResult, TaskUpdate } from "../models/task";
import type { LogFilter, LogInput, LogResult, LogUpdate } from "../models/log"; import type { LogFilter, LogInput, LogResult, LogUpdate } from "../models/log";
import type { GroupInput, GroupResult, GroupUpdate } from "../models/group"; import type { GroupInput, GroupResult, GroupUpdate } from "../models/group";
import type { ItemInput, ItemResult, ItemUpdate } from "../models/item"; import type { ItemInput, ItemResult, ItemUpdate } from "../models/item";
@@ -132,6 +132,18 @@ export class StufflogClient {
async createTaskLink(projectId: string, taskId: string): Promise<TaskLink> {
const data = await this.fetch("PUT", `/api/task/${taskId}/link/${projectId}`);
return data.taskLink;
}
async deleteTaskLink(projectId: string, taskId: string): Promise<TaskLink> {
const data = await this.fetch("DELETE", `/api/task/${taskId}/link/${projectId}`);
return data.taskLink;
}
async findTask(id: string): Promise<TaskResult> { async findTask(id: string): Promise<TaskResult> {
const data = await this.fetch("GET", `/api/task/${id}`); const data = await this.fetch("GET", `/api/task/${id}`);
return data.task; return data.task;
@@ -50,7 +50,6 @@
const boatTarget = Math.ceil(timeProgress * progressTarget); const boatTarget = Math.ceil(timeProgress * progressTarget);
boat = progressAmount - boatTarget; boat = progressAmount - boatTarget;
console.log(progressTarget, boatTarget);
} else { } else {
boat = null; boat = null;
} }
@@ -67,10 +66,6 @@
} }
} }
} }
$: {
console.log(boat);
}
</script> </script>
<div class="progress"> <div class="progress">
@@ -26,6 +26,7 @@ import stuffLogClient from "../clients/stufflog";
let mdAddTask: ModalData; let mdAddTask: ModalData;
let mdProjectEdit: ModalData; let mdProjectEdit: ModalData;
let mdProjectDelete: ModalData; let mdProjectDelete: ModalData;
let mdLinkTask: ModalData;
let linkTarget: string = ""; let linkTarget: string = "";
let activeTasks: TaskResult[] = []; let activeTasks: TaskResult[] = [];
let inactiveTasks: TaskResult[] = []; let inactiveTasks: TaskResult[] = [];
@@ -73,6 +74,7 @@ import stuffLogClient from "../clients/stufflog";
} }
$: mdAddTask = {name:"task.add", project}; $: mdAddTask = {name:"task.add", project};
$: mdLinkTask = {name:"tasklink.add", project};
$: mdProjectEdit = {name:"project.edit", project}; $: mdProjectEdit = {name:"project.edit", project};
$: mdProjectDelete = {name:"project.delete", project}; $: mdProjectDelete = {name:"project.delete", project};
@@ -114,6 +116,7 @@ import stuffLogClient from "../clients/stufflog";
{#if showAllOptions} {#if showAllOptions}
<OptionRow> <OptionRow>
<Option open={mdAddTask}>Add Task</Option> <Option open={mdAddTask}>Add Task</Option>
<Option open={mdLinkTask}>Link Task</Option>
<Option open={mdProjectEdit}>Edit</Option> <Option open={mdProjectEdit}>Edit</Option>
<Option open={mdProjectDelete}>Delete</Option> <Option open={mdProjectDelete}>Delete</Option>
{#if canComplete} {#if canComplete}
@@ -14,12 +14,6 @@
let optGroups: OptGroup[] let optGroups: OptGroup[]
$: {
if ($projectStore.stale && !$projectStore.loading) {
projectStore.load({});
}
}
$: { $: {
optGroups = [ optGroups = [
{ {
@@ -42,6 +36,14 @@
status: "Completed", status: "Completed",
projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "completed") projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "completed")
}, },
{
status: "Background",
projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "background")
},
{
status: "Progress",
projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "progress")
},
{ {
status: "Failed", status: "Failed",
projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "failed") projects: $projectStore.projects.filter(p => !p.active && p.statusTag === "failed")
+28 -20
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { tick } from "svelte"; import { tick } from "svelte";
import stuffLogClient from "../clients/stufflog"; import stuffLogClient from "../clients/stufflog";
import type Project from "../models/project"; import type Project from "../models/project";
@@ -23,7 +23,9 @@ import { tick } from "svelte";
let mdLogAdd: ModalData; let mdLogAdd: ModalData;
let mdTaskEdit: ModalData; let mdTaskEdit: ModalData;
let mdTaskDelete: ModalData; let mdTaskDelete: ModalData;
let mdTaskUnlink: ModalData;
let isMoving = false; let isMoving = false;
let isLinked = false;
function toggleShowLogs() { function toggleShowLogs() {
showLogs = !showLogs; showLogs = !showLogs;
@@ -66,6 +68,8 @@ import { tick } from "svelte";
$: mdLogAdd = {name: "log.add", task: {...task, project}}; $: mdLogAdd = {name: "log.add", task: {...task, project}};
$: mdTaskEdit = {name: "task.edit", task: {...task, project}}; $: mdTaskEdit = {name: "task.edit", task: {...task, project}};
$: mdTaskDelete = {name: "task.delete", task: {...task, project}}; $: mdTaskDelete = {name: "task.delete", task: {...task, project}};
$: mdTaskUnlink = {name: "tasklink.delete", task, project};
$: isLinked = task.projectId !== project.id;
</script> </script>
<StatusColor affects="task" entry={task}> <StatusColor affects="task" entry={task}>
@@ -81,25 +85,29 @@ import { tick } from "svelte";
<Option open={mdLogAdd}>Add Log</Option> <Option open={mdLogAdd}>Add Log</Option>
{#if showAllOptions} {#if showAllOptions}
<Option open={mdTaskEdit}>Edit</Option> <Option open={mdTaskEdit}>Edit</Option>
<Option open={mdTaskDelete}>Delete</Option> {#if isLinked}
· <Option open={mdTaskUnlink}>Unlink</Option>
{#if !isMoving && (!task.active) } {:else}
<Option on:click={moveToActive}>Active</Option> <Option open={mdTaskDelete}>Delete</Option>
{/if} ·
{#if !isMoving && (task.statusTag !== "to do") } {#if !isMoving && (!task.active) }
<Option color="yellow" on:click={moveToToDo}>To Do</Option> <Option on:click={moveToActive}>Active</Option>
{/if} {/if}
{#if !isMoving && (task.statusTag !== "on hold") } {#if !isMoving && (task.statusTag !== "to do") }
<Option color="blue" on:click={moveToOnHold}>On Hold</Option> <Option color="yellow" on:click={moveToToDo}>To Do</Option>
{/if} {/if}
{#if !isMoving && (task.statusTag !== "declined") } {#if !isMoving && (task.statusTag !== "on hold") }
<Option color="purple" on:click={moveToDeclined}>Won't Do</Option> <Option color="blue" on:click={moveToOnHold}>On Hold</Option>
{/if} {/if}
{#if !isMoving && task.active } {#if !isMoving && (task.statusTag !== "declined") }
<Option color="green" on:click={moveToCompleted}>Completed</Option> <Option color="purple" on:click={moveToDeclined}>Won't Do</Option>
{/if} {/if}
{#if !isMoving && (task.statusTag !== "failed") } {#if !isMoving && task.active }
<Option color="red" on:click={moveToFailed}>Failed</Option> <Option color="green" on:click={moveToCompleted}>Completed</Option>
{/if}
{#if !isMoving && (task.statusTag !== "failed") }
<Option color="red" on:click={moveToFailed}>Failed</Option>
{/if}
{/if} {/if}
{/if} {/if}
</OptionRow> </OptionRow>
@@ -0,0 +1,36 @@
<script lang="ts">
import projectStore from "../stores/project";
export let value = "";
export let name = "";
export let disabled = false;
export let optional = false;
$: {
if ($projectStore.stale && !$projectStore.loading) {
projectStore.load({});
}
}
$: {
if ($projectStore.projects.length > 0 && value === "" && !optional) {
const nonEmpty = $projectStore.projects.find(g => g.tasks.length > 0);
if (nonEmpty != null) {
value = nonEmpty.tasks[0].id;
}
}
}
</script>
<select name={name} bind:value={value} disabled={disabled || $projectStore.loading}>
{#if optional}
<option value={""} selected={"" === value}>None</option>
{/if}
{#each $projectStore.projects as project (project.id)}
<optgroup label={project.name}>
{#each project.tasks as task (task.id)}
<option value={task.id} selected={task.id === value}>{task.name}</option>
{/each}
</optgroup>
{/each}
</select>
+69
View File
@@ -0,0 +1,69 @@
<script lang="ts">
import stuffLogClient from "../clients/stufflog";
import Modal from "../components/Modal.svelte";
import ProjectSelect from "../components/ProjectSelect.svelte";
import TaskSelect from "../components/TaskSelect.svelte";
import markStale from "../stores/markStale";
import modalStore from "../stores/modal";
export let deletion = false;
export let creation = false;
const md = $modalStore;
let projectId = "";
let taskId = "";
let verb = "Add";
if (md.name === "tasklink.delete") {
projectId = md.project.id;
taskId = md.task.id;
verb = "Delete";
} else if (md.name === "tasklink.add") {
projectId = (md.project||{id:""}).id;
taskId = (md.task||{id:""}).id;
} else {
throw new Error(`Wrong form ${md.name}`)
}
let error = null;
let loading = false;
function onSubmit() {
loading = true;
error = null;
if (creation) {
stuffLogClient.createTaskLink(projectId, taskId).then(() => {
markStale("goal", "project");
modalStore.close();
}).catch(err => {
error = err.message ? err.message : err.toString();
}).finally(() => {
loading = false;
})
} else if (deletion) {
stuffLogClient.deleteTaskLink(projectId, taskId).then(() => {
markStale("goal", "project");
modalStore.close();
}).catch(err => {
error = err.message ? err.message : err.toString();
}).finally(() => {
loading = false;
})
}
}
</script>
<Modal show title="{verb} Link" error={error} closable on:close={modalStore.close}>
<form on:submit|preventDefault={onSubmit}>
<label for="taskId">Source Task</label>
<TaskSelect disabled={deletion} name="taskId" bind:value={taskId} />
<label for="projectId">Destination Project</label>
<ProjectSelect disabled={deletion} name="projectId" bind:value={projectId} />
<hr />
<button disabled={loading} type="submit">{verb} Link</button>
</form>
</Modal>
+6
View File
@@ -23,6 +23,12 @@ export interface TaskResult extends Task {
project?: Project project?: Project
} }
// TaskLink is only returned by the API. The tasks are returned transparently.
export interface TaskLink {
taskId: string
projectId: string
}
export interface TaskFilter { export interface TaskFilter {
active?: boolean active?: boolean
expiring?: boolean expiring?: boolean
+1 -1
View File
@@ -4,7 +4,7 @@
import projectStore from "../stores/project"; import projectStore from "../stores/project";
$: { $: {
if (($projectStore.stale || $projectStore.filter.active != null) && !$projectStore.loading) { if ($projectStore.stale && !$projectStore.loading) {
projectStore.load({}); projectStore.load({});
} }
} }
+6 -2
View File
@@ -3,8 +3,10 @@ import type { GoalResult } from "../models/goal";
import type { GroupResult } from "../models/group"; import type { GroupResult } from "../models/group";
import type { ItemResult } from "../models/item"; import type { ItemResult } from "../models/item";
import type { LogResult } from "../models/log"; import type { LogResult } from "../models/log";
import type { ProjectResult } from "../models/project"; import type {ProjectResult} from "../models/project";
import type { TaskResult } from "../models/task"; import type {TaskResult} from "../models/task";
import type Project from "../models/project";
import type Task from "../models/task";
export type ModalData = export type ModalData =
| { name: "none" } | { name: "none" }
@@ -26,6 +28,8 @@ export type ModalData =
| { name: "goal.add" } | { name: "goal.add" }
| { name: "goal.edit", goal: GoalResult } | { name: "goal.edit", goal: GoalResult }
| { name: "goal.delete", goal: GoalResult } | { name: "goal.delete", goal: GoalResult }
| { name: "tasklink.add", project?: Project, task?: Task }
| { name: "tasklink.delete", project: Project, task: Task }
function createModalStore() { function createModalStore() {
const {set, subscribe} = writable<ModalData>({name: "none"}); const {set, subscribe} = writable<ModalData>({name: "none"});