This commit is contained in:
@@ -62,6 +62,10 @@ func Project(g *gin.RouterGroup, db database.Database) {
|
|||||||
project.UserID = auth.UserID(c)
|
project.UserID = auth.UserID(c)
|
||||||
project.CreatedTime = time.Now().UTC()
|
project.CreatedTime = time.Now().UTC()
|
||||||
|
|
||||||
|
if project.GroupID != nil && *project.GroupID == "" {
|
||||||
|
project.GroupID = nil
|
||||||
|
}
|
||||||
|
|
||||||
err = db.Projects().Insert(c.Request.Context(), project)
|
err = db.Projects().Insert(c.Request.Context(), project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (r *projectGroupRepository) Find(ctx context.Context, id string) (*models.P
|
|||||||
|
|
||||||
func (r *projectGroupRepository) List(ctx context.Context, filter models.ProjectGroupFilter) ([]*models.ProjectGroup, error) {
|
func (r *projectGroupRepository) List(ctx context.Context, filter models.ProjectGroupFilter) ([]*models.ProjectGroup, error) {
|
||||||
res := make([]*projectGroupDBO, 0, 16)
|
res := make([]*projectGroupDBO, 0, 16)
|
||||||
err := r.db.SelectContext(ctx, &res, "SELECT * FROM project_group WHERE user_id=$1", filter.UserID)
|
err := r.db.SelectContext(ctx, &res, "SELECT * FROM project_group WHERE user_id=$1 ORDER BY abbreviation", filter.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return []*models.ProjectGroup{}, nil
|
return []*models.ProjectGroup{}, nil
|
||||||
|
|||||||
+3
-2
@@ -24,9 +24,10 @@ type Project struct {
|
|||||||
|
|
||||||
func (project *Project) Update(update ProjectUpdate) {
|
func (project *Project) Update(update ProjectUpdate) {
|
||||||
if update.GroupID != nil {
|
if update.GroupID != nil {
|
||||||
project.GroupID = update.GroupID
|
if *update.GroupID == "" {
|
||||||
if *project.GroupID == "" {
|
|
||||||
project.GroupID = nil
|
project.GroupID = nil
|
||||||
|
} else {
|
||||||
|
project.GroupID = update.GroupID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if update.Name != nil {
|
if update.Name != nil {
|
||||||
|
|||||||
+1
-1
@@ -442,7 +442,7 @@ func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupR
|
|||||||
result := &models.ProjectGroupResult{
|
result := &models.ProjectGroupResult{
|
||||||
ProjectGroup: models.ProjectGroup{
|
ProjectGroup: models.ProjectGroup{
|
||||||
ID: "META_UNGROUPED",
|
ID: "META_UNGROUPED",
|
||||||
Name: "Ungrouped",
|
Name: "Ungrouped Projects",
|
||||||
Abbreviation: "OTHER",
|
Abbreviation: "OTHER",
|
||||||
CategoryNames: map[string]string{},
|
CategoryNames: map[string]string{},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import GoalForm from "./forms/GoalForm.svelte";
|
import GoalForm from "./forms/GoalForm.svelte";
|
||||||
import LogForm from "./forms/LogForm.svelte";
|
import LogForm from "./forms/LogForm.svelte";
|
||||||
import ProjectForm from "./forms/ProjectForm.svelte";
|
import ProjectForm from "./forms/ProjectForm.svelte";
|
||||||
|
import ProjectGroupForm from "./forms/ProjectGroupForm.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 TaskLinkForm from "./forms/TaskLinkForm.svelte";
|
||||||
@@ -25,10 +26,12 @@
|
|||||||
import ModalRoute from "./components/ModalRoute.svelte";
|
import ModalRoute from "./components/ModalRoute.svelte";
|
||||||
import FocusHandler from "./components/FocusHandler.svelte";
|
import FocusHandler from "./components/FocusHandler.svelte";
|
||||||
import Menu from "./components/Menu.svelte";
|
import Menu from "./components/Menu.svelte";
|
||||||
|
import markStale from "./stores/markStale";
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await signOut();
|
await signOut();
|
||||||
await authStore.check();
|
await authStore.check();
|
||||||
|
markStale("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -45,6 +48,12 @@
|
|||||||
<Route path="/" component={FrontPage} />
|
<Route path="/" component={FrontPage} />
|
||||||
<Route path="/goals/" component={GoalPage} />
|
<Route path="/goals/" component={GoalPage} />
|
||||||
<Route path="/projects/" component={ProjectPage} />
|
<Route path="/projects/" component={ProjectPage} />
|
||||||
|
<Route path="/questlog/:gid/:pid" let:params >
|
||||||
|
<QlPage groupId={params.gid} projectId={params.pid} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/questlog/:gid" let:params >
|
||||||
|
<QlPage groupId={params.gid} />
|
||||||
|
</Route>
|
||||||
<Route path="/questlog/" component={QlPage} />
|
<Route path="/questlog/" component={QlPage} />
|
||||||
<Route path="/logs/" component={LogsPage} />
|
<Route path="/logs/" component={LogsPage} />
|
||||||
<Route path="/items/" component={GroupPage} />
|
<Route path="/items/" component={GroupPage} />
|
||||||
@@ -63,6 +72,9 @@
|
|||||||
<ModalRoute name="project.add"> <ProjectForm creation /></ModalRoute>
|
<ModalRoute name="project.add"> <ProjectForm creation /></ModalRoute>
|
||||||
<ModalRoute name="project.edit"> <ProjectForm /></ModalRoute>
|
<ModalRoute name="project.edit"> <ProjectForm /></ModalRoute>
|
||||||
<ModalRoute name="project.delete"> <ProjectForm deletion /></ModalRoute>
|
<ModalRoute name="project.delete"> <ProjectForm deletion /></ModalRoute>
|
||||||
|
<ModalRoute name="projectgroup.add"> <ProjectGroupForm creation /></ModalRoute>
|
||||||
|
<ModalRoute name="projectgroup.edit"> <ProjectGroupForm /></ModalRoute>
|
||||||
|
<ModalRoute name="projectgroup.delete"> <ProjectGroupForm deletion /></ModalRoute>
|
||||||
<ModalRoute name="item.add"> <ItemForm creation /></ModalRoute>
|
<ModalRoute name="item.add"> <ItemForm creation /></ModalRoute>
|
||||||
<ModalRoute name="item.edit"> <ItemForm /></ModalRoute>
|
<ModalRoute name="item.edit"> <ItemForm /></ModalRoute>
|
||||||
<ModalRoute name="item.delete"> <ItemForm deletion /></ModalRoute>
|
<ModalRoute name="item.delete"> <ItemForm deletion /></ModalRoute>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { TaskFilter, TaskInput, TaskLink, TaskResult, TaskUpdate } from "..
|
|||||||
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";
|
||||||
import type { ProjectGroupResult } from "../models/projectgroup";
|
import type { ProjectGroupInput, ProjectGroupResult, ProjectGroupUpdate } from "../models/projectgroup";
|
||||||
|
|
||||||
export class StufflogClient {
|
export class StufflogClient {
|
||||||
private root: string;
|
private root: string;
|
||||||
@@ -109,6 +109,21 @@ export class StufflogClient {
|
|||||||
return data.projectGroups;
|
return data.projectGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createProjectGroup(input: ProjectGroupInput): Promise<ProjectGroupResult> {
|
||||||
|
const data = await this.fetch("POST", "/api/projectgroup/", input);
|
||||||
|
return data.projectGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectGroup(id: string, input: ProjectGroupUpdate): Promise<ProjectGroupResult> {
|
||||||
|
const data = await this.fetch("PUT", `/api/projectgroup/${id}`, input);
|
||||||
|
return data.projectGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProjectGroup(id: string): Promise<ProjectGroupResult> {
|
||||||
|
const data = await this.fetch("DELETE", `/api/projectgroup/${id}`);
|
||||||
|
return data.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async findLog(id: string): Promise<LogResult> {
|
async findLog(id: string): Promise<LogResult> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
interface ActualParent {
|
interface ActualParent {
|
||||||
id: string
|
id: string
|
||||||
|
groupId: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
|
|
||||||
interface EntryCommonSub {
|
interface EntryCommonSub {
|
||||||
id?: string
|
id?: string
|
||||||
|
groupId?: string
|
||||||
name: string
|
name: string
|
||||||
icon: IconName
|
icon: IconName
|
||||||
project?: EntryCommonSub
|
project?: EntryCommonSub
|
||||||
@@ -64,12 +66,12 @@
|
|||||||
<Icon block name="link" />
|
<Icon block name="link" />
|
||||||
</div>
|
</div>
|
||||||
<div class="actual-parent">
|
<div class="actual-parent">
|
||||||
<a href={`/questlog#${actualParent.id}`}>{actualParent.name}</a>
|
<a href={`/questlog/${actualParent.groupId}/${actualParent.id}`}>{actualParent.name}</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if (entry.task && entry.task.project != null)}
|
{:else if (entry.task && entry.task.project != null)}
|
||||||
<div class="actual-parent">
|
<div class="actual-parent">
|
||||||
<a href={`/questlog#${entry.task.project.id}`}>{entry.task.project.name}</a>
|
<a href={`/questlog/${entry.task.project.groupId}/${entry.task.project.id}`}>{entry.task.project.name}</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="name">{displayName}</div>
|
<div class="name">{displayName}</div>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import StatusColor from "./StatusColor.svelte";
|
||||||
|
|
||||||
|
export let selected: boolean = false;
|
||||||
|
export let status: string = "to do";
|
||||||
|
export let number: number | null | undefined = void(0);
|
||||||
|
|
||||||
|
let entry: {statusTag: string}
|
||||||
|
|
||||||
|
$: entry = {statusTag: status};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if (!!number)}
|
||||||
|
<StatusColor selected={selected} affects="project" entry={entry}>
|
||||||
|
<div class="sccfg">{number}</div>
|
||||||
|
</StatusColor>
|
||||||
|
{/if}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import markStale from "../stores/markStale";
|
import markStale from "../stores/markStale";
|
||||||
|
let lastFocus = Date.now();
|
||||||
|
|
||||||
function handleFocus() {
|
function handleFocus() {
|
||||||
markStale("*");
|
if ((Date.now() - lastFocus) > 60000) {
|
||||||
|
markStale("*");
|
||||||
|
lastFocus = Date.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ div.modal :global(input), div.modal :global(select), div.modal :global(textarea)
|
|||||||
div.modal :global(select) {
|
div.modal :global(select) {
|
||||||
padding-left: 0.5ch;
|
padding-left: 0.5ch;
|
||||||
}
|
}
|
||||||
|
div.modal :global(input)::placeholder {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
div.modal :global(select:disabled) {
|
div.modal :global(select:disabled) {
|
||||||
background: #4a4a4a;
|
background: #4a4a4a;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<div class="option-row">
|
<script lang="ts">
|
||||||
|
export let centered = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="option-row" class:centered>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -7,4 +11,9 @@
|
|||||||
margin-left: -0.5ch;
|
margin-left: -0.5ch;
|
||||||
margin-right: -0.5ch;
|
margin-right: -0.5ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.option-row.centered {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
|
import { navigate } from "svelte-routing";
|
||||||
|
|
||||||
|
import type { ProjectGroupResult } from "../models/projectgroup";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
import projectGroupStore from "../stores/projectGroup";
|
||||||
|
import { sortProjects } from "../utils/sorters";
|
||||||
|
import ColoredNumber from "./ColoredNumber.svelte";
|
||||||
|
|
||||||
|
export let groups: ProjectGroupResult[] = [];
|
||||||
|
export let selected: string;
|
||||||
|
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
|
function onClickAdd() {
|
||||||
|
modalStore.set({name: "projectgroup.add"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNavigate(group: ProjectGroupResult) {
|
||||||
|
if (group.projects.length === 0) {
|
||||||
|
navigate(`/questlog/${group.id}`);
|
||||||
|
} else {
|
||||||
|
const projetcs = [...group.projects].sort(sortProjects);
|
||||||
|
|
||||||
|
navigate(`/questlog/${group.id}/${projetcs[0].id}`);
|
||||||
|
|
||||||
|
// There's some weirdness with navigate. This hack will just do a
|
||||||
|
// groups = [...groups] in the sttore to get the page to update.
|
||||||
|
tick().then(() => {
|
||||||
|
projectGroupStore.fakeRefresh();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="group-menu">
|
||||||
|
{#each groups as group (group.id)}
|
||||||
|
<div class="group-entry" on:click={() => onNavigate(group)} class:selected={selected === group.id}>
|
||||||
|
<div class="name">{group.abbreviation}</div>
|
||||||
|
<div class="counts">
|
||||||
|
<ColoredNumber selected={selected === group.id} status="active" number={group.projectCounts["active"]} />
|
||||||
|
<ColoredNumber selected={selected === group.id} status="background" number={group.projectCounts["background"]} />
|
||||||
|
<ColoredNumber selected={selected === group.id} status="progress" number={group.projectCounts["progress"]} />
|
||||||
|
<ColoredNumber selected={selected === group.id} status="to do" number={group.projectCounts["to do"]} />
|
||||||
|
<ColoredNumber selected={selected === group.id} status="on hold" number={group.projectCounts["on hold"]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="group-entry add" on:click={onClickAdd}>
|
||||||
|
<div>+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.group-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0 2ch;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.group-entry {
|
||||||
|
padding: 0.05em 1ch;
|
||||||
|
padding-bottom: 0.15em;
|
||||||
|
margin-right: 2ch;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top-left-radius: 0.5em;
|
||||||
|
border-top-right-radius: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #777;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
div.group-entry.add {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
div.group-entry.add > div {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
div.group-entry.selected {
|
||||||
|
background-color: #191919;
|
||||||
|
color: #AAA;
|
||||||
|
}
|
||||||
|
div.group-entry:hover {
|
||||||
|
color: #CCC;
|
||||||
|
background-color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.name {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0em 0.4ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.counts {
|
||||||
|
font-size: 0.666em;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
div.counts :global(div) {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0em 0.2ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import projectGroupStore from "../stores/projectGroup";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let name = "";
|
||||||
|
export let disabled = false;
|
||||||
|
export let optional = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select name={name} bind:value={value} disabled={disabled || $projectGroupStore.loading}>
|
||||||
|
{#if optional}
|
||||||
|
<option value={""} selected={"" === value}>None</option>
|
||||||
|
{/if}
|
||||||
|
{#each $projectGroupStore.groups as group (group.id)}
|
||||||
|
{#if group.id !== "META_UNGROUPED"}
|
||||||
|
<option value={group.id} selected={group.id === value}>{group.name}</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type Project from "../models/project";
|
import type Project from "../models/project";
|
||||||
import projectStore from "../stores/project";
|
import projectStore from "../stores/project";
|
||||||
|
|
||||||
interface OptGroup {
|
interface OptGroup {
|
||||||
status: string
|
status: string
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
export let projects: ProjectResult[];
|
export let projects: ProjectResult[];
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
|
export let selected: string | null = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if projects.length > 0}
|
{#if projects.length > 0}
|
||||||
<div class="ql-list">
|
<div class="ql-list">
|
||||||
<h2>{label}</h2>
|
<h2>{label}</h2>
|
||||||
{#each projects as project (project.id)}
|
{#each projects as project (project.id)}
|
||||||
<QlListItem project={project} />
|
<QlListItem selected={project.id === selected} project={project} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import App from "../App.svelte";
|
import { navigate } from "svelte-routing";
|
||||||
import type { ProjectResult } from "../models/project";
|
|
||||||
import selectionStore from "../stores/selection";
|
import type { ProjectResult } from "../models/project";
|
||||||
import DaysLeft from "./DaysLeft.svelte";
|
import DaysLeft from "./DaysLeft.svelte";
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./Icon.svelte";
|
||||||
import ProjectIcon from "./ProjectIcon.svelte";
|
|
||||||
import ProjectProgress from "./ProjectProgress.svelte";
|
import ProjectProgress from "./ProjectProgress.svelte";
|
||||||
import StatusColor from "./StatusColor.svelte";
|
import StatusColor from "./StatusColor.svelte";
|
||||||
import Tag from "./Tag.svelte";
|
import Tag from "./Tag.svelte";
|
||||||
import TimeProgress from "./TimeProgress.svelte";
|
import TimeProgress from "./TimeProgress.svelte";
|
||||||
|
|
||||||
export let project: ProjectResult;
|
export let project: ProjectResult;
|
||||||
|
export let selected: boolean;
|
||||||
|
|
||||||
let selected: boolean;
|
|
||||||
let completed: boolean;
|
let completed: boolean;
|
||||||
let deadline: boolean;
|
let deadline: boolean;
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
selectionStore.change("hash", project.id);
|
navigate(`/questlog/${project.groupId || "META_UNGROUPED"}/${project.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selected = $selectionStore.hash === project.id;
|
|
||||||
$: completed = !project.active;
|
$: completed = !project.active;
|
||||||
$: deadline = !!project.endTime;
|
$: deadline = !!project.endTime;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,11 +9,21 @@
|
|||||||
import QlList from "./QLList.svelte";
|
import QlList from "./QLList.svelte";
|
||||||
import Boi from "../components/Boi.svelte";
|
import Boi from "../components/Boi.svelte";
|
||||||
import type { ModalData } from "../stores/modal";
|
import type { ModalData } from "../stores/modal";
|
||||||
|
import type { ProjectGroupResult } from "../models/projectgroup";
|
||||||
|
import ProjectGroupMenu from "./ProjectGroupMenu.svelte";
|
||||||
|
import OptionRow from "./OptionRow.svelte";
|
||||||
|
import Option from "./Option.svelte";
|
||||||
|
import type ProjectGroup from "../models/projectgroup";
|
||||||
|
|
||||||
export let projects: ProjectResult[];
|
export let groups: ProjectGroupResult[];
|
||||||
|
export let projectId = "";
|
||||||
|
export let groupId = "";
|
||||||
|
|
||||||
const mdProjectAdd: ModalData = {name: "project.add"};
|
let mdProjectAdd: ModalData = {name: "project.add", groupId: null};
|
||||||
|
let mdGroupEdit: ModalData = {name: "projectgroup.edit", projectGroup: {} as ProjectGroup};
|
||||||
|
let mdGroupDelete: ModalData = {name: "projectgroup.delete", projectGroup: {} as ProjectGroup};
|
||||||
|
|
||||||
|
let projects: ProjectResult[] = [];
|
||||||
let expiringProjects: ProjectResult[];
|
let expiringProjects: ProjectResult[];
|
||||||
let activeProjects: ProjectResult[];
|
let activeProjects: ProjectResult[];
|
||||||
let inactiveProjects: ProjectResult[];
|
let inactiveProjects: ProjectResult[];
|
||||||
@@ -22,16 +32,29 @@
|
|||||||
let onholdProjects: ProjectResult[];
|
let onholdProjects: ProjectResult[];
|
||||||
let ideaProjects: ProjectResult[];
|
let ideaProjects: ProjectResult[];
|
||||||
let project: ProjectResult = null;
|
let project: ProjectResult = null;
|
||||||
|
let selectedGroup: ProjectGroupResult | null = null;
|
||||||
|
|
||||||
function sortProjects(a: ProjectResult, b: ProjectResult) {
|
function sortProjects(a: ProjectResult, b: ProjectResult) {
|
||||||
const aName = `${a.tags.slice(0, 1).map(t => t+":").join("")} ${a.name}`.trim();
|
const aName = `${a.tags.slice(0, 1).map(t => t+":").join("")} ${a.name}`.trim();
|
||||||
const bName = `${b.tags.slice(0, 1).map(t => t+":").join("")} ${b.name}`.trim();
|
const bName = `${b.tags.slice(0, 1).map(t => t+":").join("")} ${b.name}`.trim();
|
||||||
|
|
||||||
console.log(aName, bName)
|
|
||||||
|
|
||||||
return aName.localeCompare(bName);
|
return aName.localeCompare(bName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure a selection.
|
||||||
|
$: {
|
||||||
|
if (groupId == "" && groups.length > 0) {
|
||||||
|
groupId = groups[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedGroup = groups.find(g => g.id === groupId)
|
||||||
|
$: projects = selectedGroup?.projects || [];
|
||||||
|
|
||||||
|
$: mdProjectAdd = { name: "project.add", groupId: groupId }
|
||||||
|
$: mdGroupEdit = { name: "projectgroup.edit", projectGroup: selectedGroup }
|
||||||
|
$: mdGroupDelete = { name: "projectgroup.delete", projectGroup: selectedGroup }
|
||||||
|
|
||||||
$: project = $selectionStore.hash.startsWith("P") ? projects.find(p => p.id === $selectionStore.hash) : null;
|
$: project = $selectionStore.hash.startsWith("P") ? projects.find(p => p.id === $selectionStore.hash) : null;
|
||||||
$: expiringProjects = projects.filter(p => p.active && p.endTime).sort((a,b) => Date.parse(a.endTime) - Date.parse(b.endTime));
|
$: expiringProjects = projects.filter(p => p.active && p.endTime).sort((a,b) => Date.parse(a.endTime) - Date.parse(b.endTime));
|
||||||
$: activeProjects = projects.filter(p => p.active && !p.endTime).sort(sortProjects);
|
$: activeProjects = projects.filter(p => p.active && !p.endTime).sort(sortProjects);
|
||||||
@@ -43,40 +66,28 @@
|
|||||||
$: backgroundProjects = inactiveProjects.filter(p => p.statusTag === "background").sort(sortProjects);
|
$: backgroundProjects = inactiveProjects.filter(p => p.statusTag === "background").sort(sortProjects);
|
||||||
$: progressProjects = inactiveProjects.filter(p => p.statusTag === "progress").sort(sortProjects);
|
$: progressProjects = inactiveProjects.filter(p => p.statusTag === "progress").sort(sortProjects);
|
||||||
|
|
||||||
$: {
|
$: project = selectedGroup?.projects.find(p => p.id === projectId) || null;
|
||||||
if (project === null && projects.length > 0) {
|
|
||||||
if (lastQuest !== "") {
|
|
||||||
project = projects.find(p => p.id === lastQuest) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project === null) {
|
|
||||||
project = expiringProjects[0] || activeProjects[0] || completedProjects[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project !== null) {
|
|
||||||
selectionStore.change("hash", project.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if ($selectionStore.hash.startsWith("P")) {
|
|
||||||
lastQuest = $selectionStore.hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProjectGroupMenu selected={groupId} groups={groups} />
|
||||||
<div class="quest-log">
|
<div class="quest-log">
|
||||||
<div class="list">
|
<div class="list">
|
||||||
|
<h2>{selectedGroup?.name || ""}</h2>
|
||||||
|
{#if !!groupId && groupId !== "META_UNGROUPED"}
|
||||||
|
<OptionRow centered>
|
||||||
|
<Option open={mdGroupEdit}>Edit Group</Option>
|
||||||
|
<Option open={mdGroupDelete}>Delete Group</Option>
|
||||||
|
</OptionRow>
|
||||||
|
{/if}
|
||||||
<Boi compacter open={mdProjectAdd}>Add Project</Boi>
|
<Boi compacter open={mdProjectAdd}>Add Project</Boi>
|
||||||
<QlList label="Deadlines" projects={expiringProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["deadlines"] || "Deadlines"} projects={expiringProjects} />
|
||||||
<QlList label="Active" projects={activeProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["active"] || "Active"} projects={activeProjects} />
|
||||||
<QlList label="Background" projects={backgroundProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["background"] || "Background"} projects={backgroundProjects} />
|
||||||
<QlList label="Progress" projects={progressProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["progress"] || "Progress"} projects={progressProjects} />
|
||||||
<QlList label="To Do" projects={ideaProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["to do"] || "To Do"} projects={ideaProjects} />
|
||||||
<QlList label="On Hold" projects={onholdProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["on hold"] || "On Hold"} projects={onholdProjects} />
|
||||||
<QlList label="Completed" projects={completedProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["completed"] || "Completed"} projects={completedProjects} />
|
||||||
<QlList label="Failed" projects={failedProjects} />
|
<QlList selected={project?.id} label={selectedGroup?.categoryNames["failed"] || "Failed"} projects={failedProjects} />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{#if project != null}
|
{#if project != null}
|
||||||
@@ -96,6 +107,13 @@
|
|||||||
width: 32ch;
|
width: 32ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 200;
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
div.body {
|
div.body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 1em 1ch;
|
margin: 1em 1ch;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface EntryCommon {
|
interface EntryCommon {
|
||||||
active: boolean
|
active?: boolean
|
||||||
statusTag?: string
|
statusTag?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
hash = hash & hash;
|
hash = hash & hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(value, hash);
|
|
||||||
|
|
||||||
style = `color: hsl(${(hash << 4) % 360}, ${40 + (hash % 8)}%, ${50+(hash % 4)}%)`
|
style = `color: hsl(${(hash << 4) % 360}, ${40 + (hash % 8)}%, ${50+(hash % 4)}%)`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { navigate } from "svelte-routing";
|
||||||
|
|
||||||
import stuffLogClient from "../clients/stufflog";
|
import stuffLogClient from "../clients/stufflog";
|
||||||
import Checkbox from "../components/Checkbox.svelte";
|
import Checkbox from "../components/Checkbox.svelte";
|
||||||
import DeadlineSelect from "../components/DeadlineSelect.svelte";
|
import DeadlineSelect from "../components/DeadlineSelect.svelte";
|
||||||
import IconSelect from "../components/IconSelect.svelte";
|
import IconSelect from "../components/IconSelect.svelte";
|
||||||
import Modal from "../components/Modal.svelte";
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import ProjectGroupSelect from "../components/ProjectGroupSelect.svelte";
|
||||||
import StartTimeSelect from "../components/StartTimeSelect.svelte";
|
import StartTimeSelect from "../components/StartTimeSelect.svelte";
|
||||||
import StatusTagSelect from "../components/StatusTagSelect.svelte";
|
import StatusTagSelect from "../components/StatusTagSelect.svelte";
|
||||||
import { DEFAULT_ICON } from "../external/icons";
|
import { DEFAULT_ICON } from "../external/icons";
|
||||||
@@ -18,6 +21,7 @@
|
|||||||
const md = $modalStore;
|
const md = $modalStore;
|
||||||
let project: ProjectResult = {
|
let project: ProjectResult = {
|
||||||
id: "",
|
id: "",
|
||||||
|
groupId: null,
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
icon: DEFAULT_ICON,
|
icon: DEFAULT_ICON,
|
||||||
@@ -33,7 +37,9 @@
|
|||||||
if (md.name === "project.edit" || md.name === "project.delete") {
|
if (md.name === "project.edit" || md.name === "project.delete") {
|
||||||
project = md.project;
|
project = md.project;
|
||||||
verb = (md.name === "project.edit") ? "Edit" : "Delete";
|
verb = (md.name === "project.edit") ? "Edit" : "Delete";
|
||||||
} else if (md.name !== "project.add") {
|
} else if (md.name === "project.add") {
|
||||||
|
project.groupId = md.groupId;
|
||||||
|
} else {
|
||||||
throw new Error("Wrong form")
|
throw new Error("Wrong form")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +51,7 @@
|
|||||||
let icon = project.icon;
|
let icon = project.icon;
|
||||||
let favorite = project.favorite;
|
let favorite = project.favorite;
|
||||||
let subtractAmount = project.subtractAmount;
|
let subtractAmount = project.subtractAmount;
|
||||||
|
let groupId = project.groupId || "";
|
||||||
let error = null;
|
let error = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let tags = project.tags.join(", ");
|
let tags = project.tags.join(", ");
|
||||||
@@ -58,6 +65,7 @@
|
|||||||
if (creation) {
|
if (creation) {
|
||||||
stuffLogClient.createProject({
|
stuffLogClient.createProject({
|
||||||
active: statusTag === "",
|
active: statusTag === "",
|
||||||
|
groupId: groupId || void(0),
|
||||||
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
||||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||||
statusTag: statusTag !== "" ? statusTag : null,
|
statusTag: statusTag !== "" ? statusTag : null,
|
||||||
@@ -65,9 +73,11 @@
|
|||||||
tags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
|
tags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
|
||||||
|
|
||||||
name, description, icon, favorite,
|
name, description, icon, favorite,
|
||||||
}).then(() => {
|
}).then(newProject => {
|
||||||
markStale("project", "task");
|
markStale("project", "task");
|
||||||
modalStore.close();
|
modalStore.close();
|
||||||
|
|
||||||
|
navigate(`/questlog/${newProject.groupId || "META_UNGROUPED"}/${newProject.id}`);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error = err.message ? err.message : err.toString();
|
error = err.message ? err.message : err.toString();
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -84,6 +94,7 @@
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
stuffLogClient.updateProject(project.id, {
|
stuffLogClient.updateProject(project.id, {
|
||||||
|
groupId: groupId || "",
|
||||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||||
clearEndTime: ( endTime == "" ),
|
clearEndTime: ( endTime == "" ),
|
||||||
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
||||||
@@ -101,6 +112,8 @@
|
|||||||
markStale("log");
|
markStale("log");
|
||||||
}
|
}
|
||||||
modalStore.close();
|
modalStore.close();
|
||||||
|
|
||||||
|
navigate(`/questlog/${groupId || "META_UNGROUPED"}/${project.id}`);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error = err.message ? err.message : err.toString();
|
error = err.message ? err.message : err.toString();
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -116,6 +129,8 @@
|
|||||||
|
|
||||||
<Modal show title="{verb} Project" error={error} closable on:close={onClose}>
|
<Modal show title="{verb} Project" error={error} closable on:close={onClose}>
|
||||||
<form on:submit|preventDefault={onSubmit}>
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="group">Project Group</label>
|
||||||
|
<ProjectGroupSelect optional name="group" disabled={deletion} bind:value={groupId} />
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { navigate } from "svelte-routing";
|
||||||
|
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import Modal from "../components/Modal.svelte";
|
||||||
|
import type ProjectGroup from "../models/projectgroup";
|
||||||
|
import markStale from "../stores/markStale";
|
||||||
|
import modalStore from "../stores/modal";
|
||||||
|
|
||||||
|
export let deletion = false;
|
||||||
|
export let creation = false;
|
||||||
|
|
||||||
|
const md = $modalStore;
|
||||||
|
let group: ProjectGroup = {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
abbreviation: "",
|
||||||
|
categoryNames: {},
|
||||||
|
}
|
||||||
|
let verb = "Add";
|
||||||
|
if (md.name === "projectgroup.edit" || md.name === "projectgroup.delete") {
|
||||||
|
group = md.projectGroup;
|
||||||
|
verb = (md.name === "projectgroup.edit") ? "Edit" : "Delete";
|
||||||
|
} else if (md.name !== "projectgroup.add") {
|
||||||
|
throw new Error("Wrong form")
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = group.name;
|
||||||
|
let description = group.description;
|
||||||
|
let abbreviation = group.abbreviation;
|
||||||
|
let categoryNames = group.categoryNames || {};
|
||||||
|
let error = null;
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (creation) {
|
||||||
|
stuffLogClient.createProjectGroup({
|
||||||
|
name, abbreviation, description, categoryNames
|
||||||
|
}).then(newGroup => {
|
||||||
|
markStale("project");
|
||||||
|
modalStore.close();
|
||||||
|
navigate(`/questlog/${newGroup.id}`);
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
}).finally(() => {
|
||||||
|
loading = false;
|
||||||
|
})
|
||||||
|
} else if (deletion) {
|
||||||
|
stuffLogClient.deleteProjectGroup(group.id).then(() => {
|
||||||
|
markStale("project");
|
||||||
|
modalStore.close();
|
||||||
|
navigate("/questlog/");
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
}).finally(() => {
|
||||||
|
loading = false;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
stuffLogClient.updateProjectGroup(group.id, {
|
||||||
|
name, abbreviation, description,
|
||||||
|
setCategoryNames: categoryNames,
|
||||||
|
}).then(() => {
|
||||||
|
markStale("project");
|
||||||
|
modalStore.close();
|
||||||
|
}).catch(err => {
|
||||||
|
error = err.message ? err.message : err.toString();
|
||||||
|
}).finally(() => {
|
||||||
|
loading = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
modalStore.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal show title="{verb} Project Group" error={error} closable on:close={onClose}>
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||||
|
<label for="abbreviation">Abbreviation</label>
|
||||||
|
<input disabled={deletion} name="abbreviation" bind:value={abbreviation} />
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea disabled={deletion} name="description" bind:value={description} />
|
||||||
|
|
||||||
|
<label for="name">Custom Labels</label>
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Deadlines" bind:value={categoryNames["deadlines"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Active" bind:value={categoryNames["active"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Background" bind:value={categoryNames["background"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Progress" bind:value={categoryNames["progress"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="To Do" bind:value={categoryNames["to do"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="On Hold" bind:value={categoryNames["on hold"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Completed" bind:value={categoryNames["completed"]} />
|
||||||
|
<input disabled={deletion} name="name" type="text" placeholder="Failed" bind:value={categoryNames["failed"]} />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<button disabled={loading} type="submit">{verb} Project Group</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
@@ -3,6 +3,7 @@ import type { TaskResult } from "./task";
|
|||||||
|
|
||||||
export default interface Project {
|
export default interface Project {
|
||||||
id: string
|
id: string
|
||||||
|
groupId: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
icon: IconName
|
icon: IconName
|
||||||
@@ -29,6 +30,7 @@ export interface ProjectFilter {
|
|||||||
|
|
||||||
export interface ProjectInput {
|
export interface ProjectInput {
|
||||||
name: string
|
name: string
|
||||||
|
groupId?: string
|
||||||
description: string
|
description: string
|
||||||
icon: IconName
|
icon: IconName
|
||||||
active: boolean
|
active: boolean
|
||||||
@@ -43,6 +45,7 @@ export interface ProjectInput {
|
|||||||
export interface ProjectUpdate {
|
export interface ProjectUpdate {
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
groupId?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
active?: boolean
|
active?: boolean
|
||||||
startTime?: string | Date
|
startTime?: string | Date
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type Project from "./project";
|
import type { ProjectResult } from "./project";
|
||||||
|
|
||||||
export default interface ProjectGroup {
|
export default interface ProjectGroup {
|
||||||
id: string
|
id: string
|
||||||
@@ -8,12 +8,28 @@ export default interface ProjectGroup {
|
|||||||
categoryNames: ProjectGroupCategoryMap
|
categoryNames: ProjectGroupCategoryMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectGroupInput {
|
||||||
|
name: string
|
||||||
|
abbreviation: string
|
||||||
|
description?: string
|
||||||
|
categoryNames?: ProjectGroupCategoryMap
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectGroupUpdate {
|
||||||
|
name?: string
|
||||||
|
abbreviation?: string
|
||||||
|
description?: string
|
||||||
|
setCategoryNames?: ProjectGroupCategoryMap
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectGroupResult extends ProjectGroup {
|
export interface ProjectGroupResult extends ProjectGroup {
|
||||||
|
projects: ProjectResult[]
|
||||||
projectCounts: ProjectGroupCountMap
|
projectCounts: ProjectGroupCountMap
|
||||||
taskCounts: ProjectGroupCountMap
|
taskCounts: ProjectGroupCountMap
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectGroupCategoryMap {
|
export interface ProjectGroupCategoryMap {
|
||||||
|
"deadlines"?: string
|
||||||
"completed"?: string
|
"completed"?: string
|
||||||
"failed"?: string
|
"failed"?: string
|
||||||
"on hold"?: string
|
"on hold"?: string
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import type { ModalData } from "../stores/modal";
|
import type { ModalData } from "../stores/modal";
|
||||||
import projectStore from "../stores/project";
|
import projectStore from "../stores/project";
|
||||||
|
|
||||||
const mdProjectAdd: ModalData = {name: "project.add"};
|
const mdProjectAdd: ModalData = {name: "project.add", groupId: null};
|
||||||
let showInactive = ($projectStore.filter.active === null);
|
let showInactive = ($projectStore.filter.active === null);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
|||||||
@@ -1,17 +1,45 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
|
|
||||||
|
import { navigate } from "svelte-routing";
|
||||||
|
|
||||||
import QuestLog from "../components/QuestLog.svelte";
|
import QuestLog from "../components/QuestLog.svelte";
|
||||||
import RefreshSelection from "../components/RefreshSelection.svelte";
|
import RefreshSelection from "../components/RefreshSelection.svelte";
|
||||||
import projectStore from "../stores/project";
|
import projectGroupStore from "../stores/projectGroup";
|
||||||
|
import { sortProjects } from "../utils/sorters";
|
||||||
|
|
||||||
|
export let groupId = "";
|
||||||
|
export let projectId = "";
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($projectStore.stale && !$projectStore.loading) {
|
if ($projectGroupStore.stale && !$projectGroupStore.loading) {
|
||||||
projectStore.load({});
|
projectGroupStore.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($projectGroupStore.groups.length > 0 && groupId === "") {
|
||||||
|
const group = $projectGroupStore.groups[0];
|
||||||
|
|
||||||
|
if (group.projects.length > 0) {
|
||||||
|
const projects = [...group.projects].sort(sortProjects);
|
||||||
|
|
||||||
|
navigate(`/questlog/${group.id}/${projects[0].id}`, {replace: true});
|
||||||
|
|
||||||
|
// There's some weirdness with navigate. This hack will just do a
|
||||||
|
// groups = [...groups] in the sttore to get the page to update.
|
||||||
|
tick().then(() => {
|
||||||
|
projectGroupStore.fakeRefresh();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
navigate(`/questlog/${group.id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<QuestLog projects={$projectStore.projects} />
|
<QuestLog groups={$projectGroupStore.groups} groupId={groupId} projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
<RefreshSelection />
|
<RefreshSelection />
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import groupStore from "./group";
|
|||||||
import logStore, { fpLogStore } from "./logs";
|
import logStore, { fpLogStore } from "./logs";
|
||||||
import projectStore, { fpProjectStore, fpProjectStore2 } from "./project";
|
import projectStore, { fpProjectStore, fpProjectStore2 } from "./project";
|
||||||
import taskStore, { fpTaskStore } from "./tasks";
|
import taskStore, { fpTaskStore } from "./tasks";
|
||||||
|
import projectGroupStore from "./projectGroup";
|
||||||
|
|
||||||
type ModelName = "goal" | "project" | "task" | "group" | "item" | "log" | "*"
|
type ModelName = "goal" | "project" | "task" | "group" | "item" | "log" | "*"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export default function markStale(...models: ModelName[]) {
|
|||||||
projectStore.markStale();
|
projectStore.markStale();
|
||||||
fpProjectStore.markStale();
|
fpProjectStore.markStale();
|
||||||
fpProjectStore2.markStale();
|
fpProjectStore2.markStale();
|
||||||
|
projectGroupStore.markStale();
|
||||||
}
|
}
|
||||||
if (markAll || models.includes("task")) {
|
if (markAll || models.includes("task")) {
|
||||||
taskStore.markStale();
|
taskStore.markStale();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 Project from "../models/project";
|
||||||
import type Task from "../models/task";
|
import type Task from "../models/task";
|
||||||
|
import type ProjectGroup from "../models/projectgroup";
|
||||||
|
|
||||||
export type ModalData =
|
export type ModalData =
|
||||||
| { name: "none" }
|
| { name: "none" }
|
||||||
@@ -16,9 +17,12 @@ export type ModalData =
|
|||||||
| { name: "task.add", project: ProjectResult }
|
| { name: "task.add", project: ProjectResult }
|
||||||
| { name: "task.edit", task: TaskResult }
|
| { name: "task.edit", task: TaskResult }
|
||||||
| { name: "task.delete", task: TaskResult }
|
| { name: "task.delete", task: TaskResult }
|
||||||
| { name: "project.add" }
|
| { name: "project.add", groupId: string | null }
|
||||||
| { name: "project.edit", project: ProjectResult }
|
| { name: "project.edit", project: ProjectResult }
|
||||||
| { name: "project.delete", project: ProjectResult }
|
| { name: "project.delete", project: ProjectResult }
|
||||||
|
| { name: "projectgroup.add" }
|
||||||
|
| { name: "projectgroup.edit", projectGroup: ProjectGroup }
|
||||||
|
| { name: "projectgroup.delete", projectGroup: ProjectGroup }
|
||||||
| { name: "group.add" }
|
| { name: "group.add" }
|
||||||
| { name: "group.edit", group: GroupResult }
|
| { name: "group.edit", group: GroupResult }
|
||||||
| { name: "group.delete", group: GroupResult }
|
| { name: "group.delete", group: GroupResult }
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import stuffLogClient from "../clients/stufflog";
|
||||||
|
import type { ProjectGroupResult } from "../models/projectgroup";
|
||||||
|
|
||||||
|
interface ProjectStoreData {
|
||||||
|
loading: boolean
|
||||||
|
stale: boolean
|
||||||
|
groups: ProjectGroupResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProjectGroupStore() {
|
||||||
|
const {update, subscribe} = writable<ProjectStoreData>({
|
||||||
|
loading: false,
|
||||||
|
stale: true,
|
||||||
|
groups: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
markStale() {
|
||||||
|
update(v => ({...v, stale: true}));
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadOne(id: string) {
|
||||||
|
update(v => ({...v, loading: true}));
|
||||||
|
const group = await stuffLogClient.findProjectGroup(id);
|
||||||
|
update(v => ({...v, loading: false, groups: [
|
||||||
|
...v.groups.filter(g => g.id === id),
|
||||||
|
group,
|
||||||
|
]}));
|
||||||
|
},
|
||||||
|
|
||||||
|
fakeRefresh() {
|
||||||
|
update(v => ({...v, groups: [...v.groups]}))
|
||||||
|
},
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
update(v => ({...v, loading: true, stale: false}));
|
||||||
|
const groups = await stuffLogClient.listProjectGroups();
|
||||||
|
update(v => ({...v, loading: false, groups: groups}));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectGroupStore = createProjectGroupStore();
|
||||||
|
|
||||||
|
export default projectGroupStore;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ProjectResult } from "../models/project";
|
||||||
|
|
||||||
|
const STATUS_ORDER: (string | undefined | null)[] = [
|
||||||
|
"deadlines",
|
||||||
|
null,
|
||||||
|
void(0),
|
||||||
|
"active",
|
||||||
|
"background",
|
||||||
|
"progress",
|
||||||
|
"to do",
|
||||||
|
"on hold",
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"declined",
|
||||||
|
]
|
||||||
|
|
||||||
|
export function sortProjects(a: ProjectResult, b: ProjectResult) {
|
||||||
|
const as = STATUS_ORDER.indexOf(a.statusTag);
|
||||||
|
const bs = STATUS_ORDER.indexOf(b.statusTag);
|
||||||
|
if (as !== bs) {
|
||||||
|
return as - bs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aName = `${a.tags.slice(0, 1).map(t => t+":").join("")} ${a.name}`.trim();
|
||||||
|
const bName = `${b.tags.slice(0, 1).map(t => t+":").join("")} ${b.name}`.trim();
|
||||||
|
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user