Browse Source

add project linking.

main
Gisle Aune 4 years ago
parent
commit
2a6c04893d
  1. 60
      api/task.go
  2. 5
      database/postgres/project.go
  3. 98
      database/postgres/tasks.go
  4. 15
      migrations/postgres/20210403142913_create_table_task_link.sql
  5. 10
      models/task.go
  6. 15
      services/loader.go
  7. 3
      svelte-ui/src/App.svelte
  8. 14
      svelte-ui/src/clients/stufflog.ts
  9. 5
      svelte-ui/src/components/ProgressNumbers.svelte
  10. 3
      svelte-ui/src/components/ProjectEntry.svelte
  11. 14
      svelte-ui/src/components/ProjectSelect.svelte
  12. 48
      svelte-ui/src/components/TaskEntry.svelte
  13. 36
      svelte-ui/src/components/TaskSelect.svelte
  14. 69
      svelte-ui/src/forms/TaskLinkForm.svelte
  15. 6
      svelte-ui/src/models/task.ts
  16. 2
      svelte-ui/src/pages/QLPage.svelte
  17. 8
      svelte-ui/src/stores/modal.ts

60
api/task.go

@ -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
database/postgres/project.go

@ -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
} }

98
database/postgres/tasks.go

@ -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)
err = r.db.SelectContext(ctx, &res, query, args...)
rows := make([]tasksWithLinks, 0, 8)
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
} }

15
migrations/postgres/20210403142913_create_table_task_link.sql

@ -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
models/task.go

@ -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
} }

15
services/loader.go

@ -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
svelte-ui/src/App.svelte

@ -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}

14
svelte-ui/src/clients/stufflog.ts

@ -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;

5
svelte-ui/src/components/ProgressNumbers.svelte

@ -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">

3
svelte-ui/src/components/ProjectEntry.svelte

@ -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
svelte-ui/src/components/ProjectSelect.svelte

@ -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")

48
svelte-ui/src/components/TaskEntry.svelte

@ -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 !isMoving && (!task.active) }
<Option on:click={moveToActive}>Active</Option>
{/if}
{#if !isMoving && (task.statusTag !== "to do") }
<Option color="yellow" on:click={moveToToDo}>To Do</Option>
{/if}
{#if !isMoving && (task.statusTag !== "on hold") }
<Option color="blue" on:click={moveToOnHold}>On Hold</Option>
{/if}
{#if !isMoving && (task.statusTag !== "declined") }
<Option color="purple" on:click={moveToDeclined}>Won't Do</Option>
{/if}
{#if !isMoving && task.active }
<Option color="green" on:click={moveToCompleted}>Completed</Option>
{/if}
{#if !isMoving && (task.statusTag !== "failed") }
<Option color="red" on:click={moveToFailed}>Failed</Option>
{#if isLinked}
<Option open={mdTaskUnlink}>Unlink</Option>
{:else}
<Option open={mdTaskDelete}>Delete</Option>
·
{#if !isMoving && (!task.active) }
<Option on:click={moveToActive}>Active</Option>
{/if}
{#if !isMoving && (task.statusTag !== "to do") }
<Option color="yellow" on:click={moveToToDo}>To Do</Option>
{/if}
{#if !isMoving && (task.statusTag !== "on hold") }
<Option color="blue" on:click={moveToOnHold}>On Hold</Option>
{/if}
{#if !isMoving && (task.statusTag !== "declined") }
<Option color="purple" on:click={moveToDeclined}>Won't Do</Option>
{/if}
{#if !isMoving && task.active }
<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>

36
svelte-ui/src/components/TaskSelect.svelte

@ -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
svelte-ui/src/forms/TaskLinkForm.svelte

@ -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
svelte-ui/src/models/task.ts

@ -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

2
svelte-ui/src/pages/QLPage.svelte

@ -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({});
} }
} }

8
svelte-ui/src/stores/modal.ts

@ -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 { TaskResult } from "../models/task";
import type {ProjectResult} from "../models/project";
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"});

Loading…
Cancel
Save