Browse Source

add favorite system and other small tweaks and fixes.

main
Gisle Aune 3 years ago
parent
commit
16237a89a4
  1. 4
      api/project.go
  2. 10
      database/postgres/project.go
  3. 11
      migrations/postgres/20210227154406_add_project_column_favorite.sql
  4. 6
      models/project.go
  5. 5
      svelte-ui/src/clients/stufflog.ts
  6. 10
      svelte-ui/src/components/Boi.svelte
  7. 5
      svelte-ui/src/components/DateRangeSelect.svelte
  8. 7
      svelte-ui/src/components/FocusHandler.svelte
  9. 1
      svelte-ui/src/components/ParentEntry.svelte
  10. 46
      svelte-ui/src/components/ProjectEntry.svelte
  11. 4
      svelte-ui/src/components/ProjectSelect.svelte
  12. 5
      svelte-ui/src/components/QuestLog.svelte
  13. 2
      svelte-ui/src/forms/LogForm.svelte
  14. 17
      svelte-ui/src/forms/ProjectForm.svelte
  15. 4
      svelte-ui/src/models/project.ts
  16. 20
      svelte-ui/src/pages/FrontPage.svelte
  17. 2
      svelte-ui/src/pages/LogsPage.svelte
  18. 5
      svelte-ui/src/pages/QLPage.svelte
  19. 19
      svelte-ui/src/stores/markStale.ts
  20. 3
      svelte-ui/src/stores/project.ts

4
api/project.go

@ -23,6 +23,10 @@ func Project(g *gin.RouterGroup, db database.Database) {
if setting := c.Query("expiring"); setting != "" {
filter.Expiring = setting == "true"
}
if setting := c.Query("favorite"); setting != "" {
favorite := setting == "true"
filter.Favorite = &favorite
}
return l.ListProjects(c, filter)
}))

10
database/postgres/project.go

@ -39,6 +39,9 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
if filter.Expiring {
sq = sq.Where("end_time IS NOT NULL")
}
if filter.Favorite != nil {
sq = sq.Where(squirrel.Eq{"favorite": *filter.Favorite})
}
sq = sq.OrderBy("active", "created_time DESC")
@ -62,9 +65,9 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error {
_, err := r.db.NamedExecContext(ctx, `
INSERT INTO project(
project_id, user_id, name, description, icon, active, created_time, end_time, status_tag
project_id, user_id, name, description, icon, active, created_time, end_time, status_tag, favorite
) VALUES (
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :end_time, :status_tag
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :end_time, :status_tag, :favorite
)
`, &project)
if err != nil {
@ -82,7 +85,8 @@ func (r *projectRepository) Update(ctx context.Context, project models.Project)
icon = :icon,
active = :active,
end_time = :end_time,
status_tag = :status_tag
status_tag = :status_tag,
favorite = :favorite
WHERE project_id=:project_id
`, &project)
if err != nil {

11
migrations/postgres/20210227154406_add_project_column_favorite.sql

@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT false;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE project
DROP COLUMN favorite;
-- +goose StatementEnd

6
models/project.go

@ -15,6 +15,7 @@ type Project struct {
CreatedTime time.Time `json:"createdTime" db:"created_time"`
EndTime *time.Time `json:"endTime" db:"end_time"`
StatusTag *string `json:"statusTag" db:"status_tag"`
Favorite bool `json:"favorite" db:"favorite"`
}
func (project *Project) Update(update ProjectUpdate) {
@ -43,6 +44,9 @@ func (project *Project) Update(update ProjectUpdate) {
if update.ClearStatusTag {
project.StatusTag = nil
}
if update.Favorite != nil {
project.Favorite = *update.Favorite
}
}
type ProjectUpdate struct {
@ -54,6 +58,7 @@ type ProjectUpdate struct {
ClearEndTime bool `json:"clearEndTime"`
StatusTag *string `json:"statusTag"`
ClearStatusTag bool `json:"clearStatusTag"`
Favorite *bool `json:"favorite"`
}
type ProjectResult struct {
@ -64,6 +69,7 @@ type ProjectResult struct {
type ProjectFilter struct {
UserID string
Active *bool
Favorite *bool
Expiring bool
IDs []string
}

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

@ -60,7 +60,7 @@ export class StufflogClient {
return data.project;
}
async listProjects({active, expiring}: ProjectFilter): Promise<ProjectResult[]> {
async listProjects({active, expiring, favorite}: ProjectFilter): Promise<ProjectResult[]> {
let queries = [];
if (active != null) {
queries.push(`active=${active}`);
@ -68,6 +68,9 @@ export class StufflogClient {
if (expiring != null) {
queries.push(`expiring=${expiring}`);
}
if (favorite != null) {
queries.push(`favorite=${favorite}`)
}
const query = queries.length > 0 ? `?${queries.join("&")}` : "";

10
svelte-ui/src/components/Boi.svelte

@ -6,6 +6,7 @@
export let open: ModalData = {name: "none"};
export let disabled: boolean = false;
export let compact: boolean = false;
export let compacter: boolean = false;
export let tiny: boolean = false;
const dispatch = createEventDispatcher();
@ -19,7 +20,7 @@
}
</script>
<div class="boi" class:disabled class:compact class:tiny on:click={handleClick}><slot></slot></div>
<div class="boi" class:disabled class:compact class:compacter class:tiny on:click={handleClick}><slot></slot></div>
<style>
div.boi {
@ -52,6 +53,13 @@
padding: 0.25em;
}
div.boi.compacter {
font-size: 1em;
margin: 0 1ch;
border-width: 3px;
padding: 0.25em;
}
div.boi.tiny {
display: inline-block;
margin: auto;

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

@ -12,7 +12,7 @@ import EveryMinute from "./EveryMinute.svelte";
export let fromValue: string;
export let toValue: string;
export let disabled: boolean;
export let disabled: boolean = false;
export let styled: boolean;
export let noFuture: boolean;
export let label: string = "Time Period";
@ -23,8 +23,11 @@ import EveryMinute from "./EveryMinute.svelte";
onMount(() => {
for (const option of options) {
console.log(formatFormTime(option.from), fromValue, formatFormTime(option.to), toValue);
if (formatFormTime(option.from) === fromValue && formatFormTime(option.to) === toValue) {
selected = option.id;
console.log("-- SELECTED")
break;
}
}
});

7
svelte-ui/src/components/FocusHandler.svelte

@ -2,6 +2,7 @@
import { onMount } from "svelte";
import goalStore, { fpGoalStore } from "../stores/goal";
import logStore from "../stores/logs";
import markStale from "../stores/markStale";
import projectStore, { fpProjectStore } from "../stores/project";
let lastFocus = new Date();
@ -13,11 +14,7 @@
lastFocus = new Date();
goalStore.markStale();
fpGoalStore.markStale();
projectStore.markStale();
fpProjectStore.markStale();
logStore.markStale();
markStale("*");
}
onMount(() => {

1
svelte-ui/src/components/ParentEntry.svelte

@ -72,6 +72,7 @@ import TimeProgress from "./TimeProgress.svelte";
{entry.name}
{/if}
</div>
<slot name="pre-annotation"></slot>
{#each annotations as annotation (annotation)}
<div class="annotation">
<Icon block name={annotation} />

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

@ -1,8 +1,12 @@
<script lang="ts">
import stuffLogClient from "../clients/stufflog";
import type { ProjectResult } from "../models/project";
import type { TaskResult } from "../models/task";
import markStale from "../stores/markStale";
import type { ModalData } from "../stores/modal";
import IS_MOBILE from "../utils/phone-check";
import Icon from "./Icon.svelte";
import Option from "./Option.svelte";
import OptionRow from "./OptionRow.svelte";
import ParentEntry from "./ParentEntry.svelte";
@ -29,6 +33,24 @@
let onholdTasks: TaskResult[] = [];
let failedTasks: TaskResult[] = [];
let nonHiddenTasks: TaskResult[] = [];
let toggling = false;
function toggleFavorite() {
if (toggling) {
return
}
toggling = true
stuffLogClient.updateProject(project.id, {
favorite: !project.favorite,
}).then(() => {
markStale("project");
}).catch(err => {
console.warn("Failed to toggle favorite:", err);
}).finally(() => {
toggling = false;
})
}
$: mdAddTask = {name:"task.add", project};
$: mdProjectEdit = {name:"project.edit", project};
@ -58,8 +80,12 @@
hideIcon={hideIcon}
showTimeProgress={!hideProgress}
removeHook={removeHook}
annotations={isFake ? ["list"] : []}
>
<div slot="pre-annotation" class="favorite" class:enabled={project.favorite} class:toggling on:click={toggleFavorite}>
{#if !isFake}
<Icon block name="star" />
{/if}
</div>
{#if showAllOptions}
<OptionRow>
<Option open={mdAddTask}>Add Task</Option>
@ -78,3 +104,21 @@
{/if}
</ParentEntry>
</StatusColor>
<style>
div.favorite {
margin: auto 0;
padding: 0 0.5ch;
line-height: 0em;
color: #333;
cursor: pointer;
}
div.favorite.enabled {
color: #e7e55e
}
div.favorite.toggling {
opacity: 0.5;
}
</style>

4
svelte-ui/src/components/ProjectSelect.svelte

@ -1,7 +1,5 @@
<script lang="ts">
import { group } from "console";
import type Project from "../models/project";
import authStore from "../stores/auth";
import type Project from "../models/project";
import projectStore from "../stores/project";
interface OptGroup {

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

@ -7,8 +7,12 @@
import selectionStore from "../stores/selection";
import ProjectEntry from "./ProjectEntry.svelte";
import QlList from "./QLList.svelte";
import Boi from "../components/Boi.svelte";
import type { ModalData } from "../stores/modal";
export let projects: ProjectResult[];
const mdProjectAdd: ModalData = {name: "project.add"};
let expiringProjects: ProjectResult[];
let activeProjects: ProjectResult[];
@ -53,6 +57,7 @@
<div class="quest-log">
<div class="list">
<Boi compacter open={mdProjectAdd}>Add Project</Boi>
<QlList label="Deadlines" projects={expiringProjects} />
<QlList label="Active" projects={activeProjects} />
<QlList label="To Do" projects={ideaProjects} />

2
svelte-ui/src/forms/LogForm.svelte

@ -119,7 +119,7 @@ import ItemSelect from "../components/ItemSelect.svelte";
<input disabled={deletion} name="secondaryItemAmount" type="number" bind:value={secondaryItemAmount} />
{/if}
<Checkbox disabled={deletion} bind:checked={markInactive} label="Mark task inactive/completed." />
<Checkbox disabled={!creation} bind:checked={markInactive} label="Mark task inactive/completed." />
<hr />

17
svelte-ui/src/forms/ProjectForm.svelte

@ -1,5 +1,6 @@
<script lang="ts">
import stuffLogClient from "../clients/stufflog";
import Checkbox from "../components/Checkbox.svelte";
import IconSelect from "../components/IconSelect.svelte";
import Modal from "../components/Modal.svelte";
import StatusTagSelect from "../components/StatusTagSelect.svelte";
@ -23,6 +24,7 @@
statusTag: "to do",
createdTime: "",
tasks: [],
favorite: false,
}
let verb = "Add";
if (md.name === "project.edit" || md.name === "project.delete") {
@ -37,6 +39,7 @@
let description = project.description;
let statusTag = project.statusTag || "";
let icon = project.icon;
let favorite = project.favorite;
let error = null;
let loading = false;
@ -44,13 +47,15 @@
loading = true;
error = null;
const iconChanged = icon !== project.icon;
if (creation) {
stuffLogClient.createProject({
active: statusTag === "",
endTime: ( endTime == "" ) ? null : new Date(endTime),
statusTag: statusTag || null,
name, description, icon,
name, description, icon, favorite,
}).then(() => {
markStale("project", "task");
modalStore.close();
@ -76,10 +81,12 @@
statusTag: statusTag || null,
clearStatusTag: statusTag === "",
name, description, icon,
name, description, icon, favorite,
}).then(() => {
projectStore.markStale();
fpProjectStore.markStale();
markStale("project", "task");
if (iconChanged) {
markStale("log");
}
modalStore.close();
}).catch(err => {
error = err.message ? err.message : err.toString();
@ -107,6 +114,8 @@
<label for="statusTag">Status</label>
<StatusTagSelect disabled={deletion} bind:value={statusTag} />
<Checkbox disabled={deletion} bind:checked={favorite} label="Mark as favorite." />
<hr />
<button disabled={loading} type="submit">{verb} Project</button>

4
svelte-ui/src/models/project.ts

@ -8,6 +8,7 @@ export default interface Project {
icon: IconName
active: boolean
createdTime: string
favorite: boolean
endTime?: string
statusTag?: string
}
@ -18,6 +19,7 @@ export interface ProjectResult extends Project {
export interface ProjectFilter {
active?: boolean
favorite?: boolean
expiring?: boolean
}
@ -28,6 +30,7 @@ export interface ProjectInput {
active: boolean
endTime?: string | Date
statusTag?: string
favorite?: boolean
}
export interface ProjectUpdate {
@ -39,4 +42,5 @@ export interface ProjectUpdate {
clearEndTime?: boolean
statusTag?: string
clearStatusTag?: boolean
favorite?: boolean
}

20
svelte-ui/src/pages/FrontPage.svelte

@ -5,7 +5,7 @@
import RefreshSelection from "../components/RefreshSelection.svelte";
import type { ProjectResult } from "../models/project";
import { fpGoalStore } from "../stores/goal";
import { fpProjectStore } from "../stores/project";
import { fpProjectStore, fpProjectStore2 } from "../stores/project";
import { fpTaskStore } from "../stores/tasks";
let fakeMap: {[projectId: string]: boolean} = {}
@ -39,6 +39,14 @@
}
}
$: {
if ($fpProjectStore2.stale && !$fpProjectStore2.loading) {
fpProjectStore2.load({
favorite: true,
});
}
}
$: {
const individualTasks = $fpTaskStore.tasks
.filter(t => $fpProjectStore.projects.find(p => p.id === t.projectId) == null)
@ -92,11 +100,15 @@
<h1>Upcoming Deadlines</h1>
{/if}
{#each sortedProjects as project (project.id)}
<ProjectEntry isFake={fakeMap[project.id]} linkProject hideInactive project={project} />
<ProjectEntry isFake={fakeMap[project.id]} hideInactive project={project} />
{/each}
{#if !$fpProjectStore.loading && $fpProjectStore.projects.length === 0}
<EmptyList icon="check" text="All good!" />
{#if !$fpProjectStore2.loading || $fpProjectStore2.projects.length > 0}
<h1>Starred Projects</h1>
{/if}
{#each $fpProjectStore2.projects as project (project.id)}
<ProjectEntry hideInactive project={project} />
{/each}
</div>
</div>
<RefreshSelection />

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

@ -10,7 +10,7 @@
let groupedLogs: {day: number, text: string, logs: LogResult[]}[] = [];
let minTime = formatFormTime($logStore.filter.minTime || startOfWeek(new Date()));
let maxTime = formatFormTime(endOfWeek(new Date()));
let maxTime = formatFormTime($logStore.filter.maxTime || endOfWeek(new Date()));
let emptyMessage = `No logs since ${formatWeekdayDate(minTime)}.`;
let error = "";
let now = new Date();

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

@ -1,12 +1,8 @@
<script lang="ts">
import Boi from "../components/Boi.svelte";
import QuestLog from "../components/QuestLog.svelte";
import RefreshSelection from "../components/RefreshSelection.svelte";
import type { ModalData } from "../stores/modal";
import projectStore from "../stores/project";
const mdProjectAdd: ModalData = {name: "project.add"};
$: {
if (($projectStore.stale || $projectStore.filter.active != null) && !$projectStore.loading) {
projectStore.load({});
@ -16,7 +12,6 @@
<div class="page">
<QuestLog projects={$projectStore.projects} />
<Boi open={mdProjectAdd}>Add Project</Boi>
</div>
<RefreshSelection />

19
svelte-ui/src/stores/markStale.ts

@ -1,31 +1,34 @@
import goalStore, { fpGoalStore } from "./goal";
import groupStore from "./group";
import logStore from "./logs";
import projectStore, { fpProjectStore } from "./project";
import projectStore, { fpProjectStore, fpProjectStore2 } from "./project";
import taskStore, { fpTaskStore } from "./tasks";
type ModelName = "goal" | "project" | "task" | "group" | "item" | "log"
type ModelName = "goal" | "project" | "task" | "group" | "item" | "log" | "*"
export default function markStale(...models: ModelName[]) {
if (models.includes("goal")) {
const markAll = models.includes("*");
if (markAll || models.includes("goal")) {
goalStore.markStale();
fpGoalStore.markStale();
}
if (models.includes("project")) {
if (markAll || models.includes("project")) {
projectStore.markStale();
fpProjectStore.markStale();
fpProjectStore2.markStale();
}
if (models.includes("task")) {
if (markAll || models.includes("task")) {
taskStore.markStale();
fpTaskStore.markStale();
}
if (models.includes("group")) {
if (markAll || models.includes("group")) {
groupStore.markStale();
}
if (models.includes("item")) {
if (markAll || models.includes("item")) {
// Do nothing.
}
if (models.includes("log")) {
if (markAll || models.includes("log")) {
logStore.markStale();
}
}

3
svelte-ui/src/stores/project.ts

@ -35,4 +35,5 @@ function createProjectStore() {
const projectStore = createProjectStore();
export default projectStore;
export const fpProjectStore = createProjectStore();
export const fpProjectStore = createProjectStore();
export const fpProjectStore2 = createProjectStore();
Loading…
Cancel
Save