This commit is contained in:
@@ -62,6 +62,10 @@ func Project(g *gin.RouterGroup, db database.Database) {
|
||||
project.UserID = auth.UserID(c)
|
||||
project.CreatedTime = time.Now().UTC()
|
||||
|
||||
if project.GroupID != nil && *project.GroupID == "" {
|
||||
project.GroupID = nil
|
||||
}
|
||||
|
||||
err = db.Projects().Insert(c.Request.Context(), project)
|
||||
if err != nil {
|
||||
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) {
|
||||
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 == sql.ErrNoRows {
|
||||
return []*models.ProjectGroup{}, nil
|
||||
|
||||
+3
-2
@@ -24,9 +24,10 @@ type Project struct {
|
||||
|
||||
func (project *Project) Update(update ProjectUpdate) {
|
||||
if update.GroupID != nil {
|
||||
project.GroupID = update.GroupID
|
||||
if *project.GroupID == "" {
|
||||
if *update.GroupID == "" {
|
||||
project.GroupID = nil
|
||||
} else {
|
||||
project.GroupID = update.GroupID
|
||||
}
|
||||
}
|
||||
if update.Name != nil {
|
||||
|
||||
+1
-1
@@ -442,7 +442,7 @@ func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupR
|
||||
result := &models.ProjectGroupResult{
|
||||
ProjectGroup: models.ProjectGroup{
|
||||
ID: "META_UNGROUPED",
|
||||
Name: "Ungrouped",
|
||||
Name: "Ungrouped Projects",
|
||||
Abbreviation: "OTHER",
|
||||
CategoryNames: map[string]string{},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import GoalForm from "./forms/GoalForm.svelte";
|
||||
import LogForm from "./forms/LogForm.svelte";
|
||||
import ProjectForm from "./forms/ProjectForm.svelte";
|
||||
import ProjectGroupForm from "./forms/ProjectGroupForm.svelte";
|
||||
import ItemForm from "./forms/ItemForm.svelte";
|
||||
import TaskForm from "./forms/TaskForm.svelte";
|
||||
import TaskLinkForm from "./forms/TaskLinkForm.svelte";
|
||||
@@ -25,10 +26,12 @@
|
||||
import ModalRoute from "./components/ModalRoute.svelte";
|
||||
import FocusHandler from "./components/FocusHandler.svelte";
|
||||
import Menu from "./components/Menu.svelte";
|
||||
import markStale from "./stores/markStale";
|
||||
|
||||
async function logout() {
|
||||
await signOut();
|
||||
await authStore.check();
|
||||
markStale("*");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -45,6 +48,12 @@
|
||||
<Route path="/" component={FrontPage} />
|
||||
<Route path="/goals/" component={GoalPage} />
|
||||
<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="/logs/" component={LogsPage} />
|
||||
<Route path="/items/" component={GroupPage} />
|
||||
@@ -63,6 +72,9 @@
|
||||
<ModalRoute name="project.add"> <ProjectForm creation /></ModalRoute>
|
||||
<ModalRoute name="project.edit"> <ProjectForm /></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.edit"> <ItemForm /></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 { GroupInput, GroupResult, GroupUpdate } from "../models/group";
|
||||
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 {
|
||||
private root: string;
|
||||
@@ -109,6 +109,21 @@ export class StufflogClient {
|
||||
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> {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
interface ActualParent {
|
||||
id: string
|
||||
groupId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@
|
||||
|
||||
interface EntryCommonSub {
|
||||
id?: string
|
||||
groupId?: string
|
||||
name: string
|
||||
icon: IconName
|
||||
project?: EntryCommonSub
|
||||
@@ -64,12 +66,12 @@
|
||||
<Icon block name="link" />
|
||||
</div>
|
||||
<div class="actual-parent">
|
||||
<a href={`/questlog#${actualParent.id}`}>{actualParent.name}</a>
|
||||
<a href={`/questlog/${actualParent.groupId}/${actualParent.id}`}>{actualParent.name}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if (entry.task && entry.task.project != null)}
|
||||
<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>
|
||||
{/if}
|
||||
<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">
|
||||
import { onMount } from "svelte";
|
||||
import markStale from "../stores/markStale";
|
||||
let lastFocus = Date.now();
|
||||
|
||||
function handleFocus() {
|
||||
markStale("*");
|
||||
if ((Date.now() - lastFocus) > 60000) {
|
||||
markStale("*");
|
||||
lastFocus = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -169,6 +169,9 @@ div.modal :global(input), div.modal :global(select), div.modal :global(textarea)
|
||||
div.modal :global(select) {
|
||||
padding-left: 0.5ch;
|
||||
}
|
||||
div.modal :global(input)::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
div.modal :global(select:disabled) {
|
||||
background: #4a4a4a;
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -7,4 +11,9 @@
|
||||
margin-left: -0.5ch;
|
||||
margin-right: -0.5ch;
|
||||
}
|
||||
|
||||
div.option-row.centered {
|
||||
text-align: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
</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">
|
||||
import type Project from "../models/project";
|
||||
import projectStore from "../stores/project";
|
||||
|
||||
interface OptGroup {
|
||||
status: string
|
||||
projects: Project[]
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
export let projects: ProjectResult[];
|
||||
export let label: string = "";
|
||||
export let selected: string | null = null;
|
||||
</script>
|
||||
|
||||
{#if projects.length > 0}
|
||||
<div class="ql-list">
|
||||
<h2>{label}</h2>
|
||||
{#each projects as project (project.id)}
|
||||
<QlListItem project={project} />
|
||||
<QlListItem selected={project.id === selected} project={project} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
<script lang="ts">
|
||||
import App from "../App.svelte";
|
||||
import type { ProjectResult } from "../models/project";
|
||||
import selectionStore from "../stores/selection";
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import type { ProjectResult } from "../models/project";
|
||||
import DaysLeft from "./DaysLeft.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import ProjectIcon from "./ProjectIcon.svelte";
|
||||
import ProjectProgress from "./ProjectProgress.svelte";
|
||||
import StatusColor from "./StatusColor.svelte";
|
||||
import Tag from "./Tag.svelte";
|
||||
import Tag from "./Tag.svelte";
|
||||
import TimeProgress from "./TimeProgress.svelte";
|
||||
|
||||
export let project: ProjectResult;
|
||||
export let selected: boolean;
|
||||
|
||||
let selected: boolean;
|
||||
let completed: boolean;
|
||||
let deadline: boolean;
|
||||
|
||||
function handleClick() {
|
||||
selectionStore.change("hash", project.id);
|
||||
navigate(`/questlog/${project.groupId || "META_UNGROUPED"}/${project.id}`);
|
||||
}
|
||||
|
||||
$: selected = $selectionStore.hash === project.id;
|
||||
$: completed = !project.active;
|
||||
$: deadline = !!project.endTime;
|
||||
</script>
|
||||
|
||||
@@ -9,11 +9,21 @@
|
||||
import QlList from "./QLList.svelte";
|
||||
import Boi from "../components/Boi.svelte";
|
||||
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 activeProjects: ProjectResult[];
|
||||
let inactiveProjects: ProjectResult[];
|
||||
@@ -22,16 +32,29 @@
|
||||
let onholdProjects: ProjectResult[];
|
||||
let ideaProjects: ProjectResult[];
|
||||
let project: ProjectResult = null;
|
||||
let selectedGroup: ProjectGroupResult | null = null;
|
||||
|
||||
function sortProjects(a: ProjectResult, b: ProjectResult) {
|
||||
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();
|
||||
|
||||
console.log(aName, 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;
|
||||
$: 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);
|
||||
@@ -43,40 +66,28 @@
|
||||
$: backgroundProjects = inactiveProjects.filter(p => p.statusTag === "background").sort(sortProjects);
|
||||
$: progressProjects = inactiveProjects.filter(p => p.statusTag === "progress").sort(sortProjects);
|
||||
|
||||
$: {
|
||||
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;
|
||||
}
|
||||
}
|
||||
$: project = selectedGroup?.projects.find(p => p.id === projectId) || null;
|
||||
</script>
|
||||
|
||||
<ProjectGroupMenu selected={groupId} groups={groups} />
|
||||
<div class="quest-log">
|
||||
<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>
|
||||
<QlList label="Deadlines" projects={expiringProjects} />
|
||||
<QlList label="Active" projects={activeProjects} />
|
||||
<QlList label="Background" projects={backgroundProjects} />
|
||||
<QlList label="Progress" projects={progressProjects} />
|
||||
<QlList label="To Do" projects={ideaProjects} />
|
||||
<QlList label="On Hold" projects={onholdProjects} />
|
||||
<QlList label="Completed" projects={completedProjects} />
|
||||
<QlList label="Failed" projects={failedProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["deadlines"] || "Deadlines"} projects={expiringProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["active"] || "Active"} projects={activeProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["background"] || "Background"} projects={backgroundProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["progress"] || "Progress"} projects={progressProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["to do"] || "To Do"} projects={ideaProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["on hold"] || "On Hold"} projects={onholdProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["completed"] || "Completed"} projects={completedProjects} />
|
||||
<QlList selected={project?.id} label={selectedGroup?.categoryNames["failed"] || "Failed"} projects={failedProjects} />
|
||||
</div>
|
||||
<div class="body">
|
||||
{#if project != null}
|
||||
@@ -96,6 +107,13 @@
|
||||
width: 32ch;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 200;
|
||||
margin: 0;
|
||||
padding-bottom: 0.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.body {
|
||||
flex-grow: 1;
|
||||
margin: 1em 1ch;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
interface EntryCommon {
|
||||
active: boolean
|
||||
active?: boolean
|
||||
statusTag?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
console.log(value, hash);
|
||||
|
||||
style = `color: hsl(${(hash << 4) % 360}, ${40 + (hash % 8)}%, ${50+(hash % 4)}%)`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Checkbox from "../components/Checkbox.svelte";
|
||||
import DeadlineSelect from "../components/DeadlineSelect.svelte";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import ProjectGroupSelect from "../components/ProjectGroupSelect.svelte";
|
||||
import StartTimeSelect from "../components/StartTimeSelect.svelte";
|
||||
import StatusTagSelect from "../components/StatusTagSelect.svelte";
|
||||
import { DEFAULT_ICON } from "../external/icons";
|
||||
@@ -18,6 +21,7 @@
|
||||
const md = $modalStore;
|
||||
let project: ProjectResult = {
|
||||
id: "",
|
||||
groupId: null,
|
||||
name: "",
|
||||
description: "",
|
||||
icon: DEFAULT_ICON,
|
||||
@@ -33,7 +37,9 @@
|
||||
if (md.name === "project.edit" || md.name === "project.delete") {
|
||||
project = md.project;
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -45,6 +51,7 @@
|
||||
let icon = project.icon;
|
||||
let favorite = project.favorite;
|
||||
let subtractAmount = project.subtractAmount;
|
||||
let groupId = project.groupId || "";
|
||||
let error = null;
|
||||
let loading = false;
|
||||
let tags = project.tags.join(", ");
|
||||
@@ -58,6 +65,7 @@
|
||||
if (creation) {
|
||||
stuffLogClient.createProject({
|
||||
active: statusTag === "",
|
||||
groupId: groupId || void(0),
|
||||
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||
statusTag: statusTag !== "" ? statusTag : null,
|
||||
@@ -65,9 +73,11 @@
|
||||
tags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
|
||||
|
||||
name, description, icon, favorite,
|
||||
}).then(() => {
|
||||
}).then(newProject => {
|
||||
markStale("project", "task");
|
||||
modalStore.close();
|
||||
|
||||
navigate(`/questlog/${newProject.groupId || "META_UNGROUPED"}/${newProject.id}`);
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
}).finally(() => {
|
||||
@@ -84,6 +94,7 @@
|
||||
})
|
||||
} else {
|
||||
stuffLogClient.updateProject(project.id, {
|
||||
groupId: groupId || "",
|
||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||
clearEndTime: ( endTime == "" ),
|
||||
startTime: ( startTime == "" ) ? null : new Date(startTime),
|
||||
@@ -101,6 +112,8 @@
|
||||
markStale("log");
|
||||
}
|
||||
modalStore.close();
|
||||
|
||||
navigate(`/questlog/${groupId || "META_UNGROUPED"}/${project.id}`);
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
}).finally(() => {
|
||||
@@ -116,6 +129,8 @@
|
||||
|
||||
<Modal show title="{verb} Project" error={error} closable on:close={onClose}>
|
||||
<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>
|
||||
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||
<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 {
|
||||
id: string
|
||||
groupId: string | null
|
||||
name: string
|
||||
description: string
|
||||
icon: IconName
|
||||
@@ -29,6 +30,7 @@ export interface ProjectFilter {
|
||||
|
||||
export interface ProjectInput {
|
||||
name: string
|
||||
groupId?: string
|
||||
description: string
|
||||
icon: IconName
|
||||
active: boolean
|
||||
@@ -43,6 +45,7 @@ export interface ProjectInput {
|
||||
export interface ProjectUpdate {
|
||||
name?: string
|
||||
description?: string
|
||||
groupId?: string
|
||||
icon?: string
|
||||
active?: boolean
|
||||
startTime?: string | Date
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type Project from "./project";
|
||||
import type { ProjectResult } from "./project";
|
||||
|
||||
export default interface ProjectGroup {
|
||||
id: string
|
||||
@@ -8,12 +8,28 @@ export default interface ProjectGroup {
|
||||
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 {
|
||||
projects: ProjectResult[]
|
||||
projectCounts: ProjectGroupCountMap
|
||||
taskCounts: ProjectGroupCountMap
|
||||
}
|
||||
|
||||
export interface ProjectGroupCategoryMap {
|
||||
"deadlines"?: string
|
||||
"completed"?: string
|
||||
"failed"?: string
|
||||
"on hold"?: string
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { ModalData } from "../stores/modal";
|
||||
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);
|
||||
|
||||
$: {
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import QuestLog from "../components/QuestLog.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) {
|
||||
projectStore.load({});
|
||||
if ($projectGroupStore.stale && !$projectGroupStore.loading) {
|
||||
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>
|
||||
|
||||
<div class="page">
|
||||
<QuestLog projects={$projectStore.projects} />
|
||||
<QuestLog groups={$projectGroupStore.groups} groupId={groupId} projectId={projectId} />
|
||||
</div>
|
||||
<RefreshSelection />
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import groupStore from "./group";
|
||||
import logStore, { fpLogStore } from "./logs";
|
||||
import projectStore, { fpProjectStore, fpProjectStore2 } from "./project";
|
||||
import taskStore, { fpTaskStore } from "./tasks";
|
||||
import projectGroupStore from "./projectGroup";
|
||||
|
||||
type ModelName = "goal" | "project" | "task" | "group" | "item" | "log" | "*"
|
||||
|
||||
@@ -17,6 +18,7 @@ export default function markStale(...models: ModelName[]) {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
fpProjectStore2.markStale();
|
||||
projectGroupStore.markStale();
|
||||
}
|
||||
if (markAll || models.includes("task")) {
|
||||
taskStore.markStale();
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {ProjectResult} from "../models/project";
|
||||
import type {TaskResult} from "../models/task";
|
||||
import type Project from "../models/project";
|
||||
import type Task from "../models/task";
|
||||
import type ProjectGroup from "../models/projectgroup";
|
||||
|
||||
export type ModalData =
|
||||
| { name: "none" }
|
||||
@@ -16,9 +17,12 @@ export type ModalData =
|
||||
| { name: "task.add", project: ProjectResult }
|
||||
| { name: "task.edit", task: TaskResult }
|
||||
| { name: "task.delete", task: TaskResult }
|
||||
| { name: "project.add" }
|
||||
| { name: "project.add", groupId: string | null }
|
||||
| { name: "project.edit", 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.edit", 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