Browse Source

add project tags.

main
Gisle Aune 4 years ago
parent
commit
5a508fe8bb
  1. 90
      database/postgres/project.go
  2. 11
      migrations/postgres/20210418164739_add_project_column_tags.sql
  3. 5
      models/project.go
  4. 30
      models/projectgroup.go
  5. 1
      svelte-ui/src/components/ParentEntry.svelte
  6. 4
      svelte-ui/src/components/ProjectEntry.svelte
  7. 7
      svelte-ui/src/components/QLListItem.svelte
  8. 33
      svelte-ui/src/components/Tag.svelte
  9. 18
      svelte-ui/src/components/TagList.svelte
  10. 6
      svelte-ui/src/forms/ProjectForm.svelte
  11. 3
      svelte-ui/src/models/project.ts

90
database/postgres/project.go

@ -7,6 +7,8 @@ import (
"github.com/gissleh/stufflog/internal/slerrors" "github.com/gissleh/stufflog/internal/slerrors"
"github.com/gissleh/stufflog/models" "github.com/gissleh/stufflog/models"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/lib/pq"
"time"
) )
type projectRepository struct { type projectRepository struct {
@ -14,17 +16,17 @@ type projectRepository struct {
} }
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) { func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) {
res := models.Project{}
res := projectDBO{}
err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id) err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, slerrors.NotFound("Log")
return nil, slerrors.NotFound("Project")
} }
return nil, err return nil, err
} }
return &res, nil
return res.ToProject(), nil
} }
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) { func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) {
@ -60,26 +62,35 @@ func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilte
return nil, err return nil, err
} }
res := make([]*models.Project, 0, 8)
res := make([]*projectDBO, 0, 8)
err = r.db.SelectContext(ctx, &res, query, args...) err = r.db.SelectContext(ctx, &res, query, args...)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return res, nil
return []*models.Project{}, nil
} }
return nil, err return nil, err
} }
return res, nil
res2 := make([]*models.Project, len(res))
for i, v := range res {
res2[i] = v.ToProject()
}
return res2, nil
} }
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error { func (r *projectRepository) Insert(ctx context.Context, project models.Project) error {
if project.Tags == nil {
project.Tags = []string{}
}
_, err := r.db.NamedExecContext(ctx, ` _, err := r.db.NamedExecContext(ctx, `
INSERT INTO project( INSERT INTO project(
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite
project_id, user_id, name, description, icon, active, created_time, start_time, end_time, subtract_amount, status_tag, favorite, tags
) VALUES ( ) VALUES (
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :start_time, :end_time, :subtract_amount, :status_tag, :favorite, :tags
) )
`, &project)
`, toProjectDBO(project))
if err != nil { if err != nil {
return err return err
} }
@ -88,6 +99,10 @@ func (r *projectRepository) Insert(ctx context.Context, project models.Project)
} }
func (r *projectRepository) Update(ctx context.Context, project models.Project) error { func (r *projectRepository) Update(ctx context.Context, project models.Project) error {
if project.Tags == nil {
project.Tags = []string{}
}
_, err := r.db.NamedExecContext(ctx, ` _, err := r.db.NamedExecContext(ctx, `
UPDATE project SET UPDATE project SET
name = :name, name = :name,
@ -98,9 +113,10 @@ func (r *projectRepository) Update(ctx context.Context, project models.Project)
end_time = :end_time, end_time = :end_time,
subtract_amount = :subtract_amount, subtract_amount = :subtract_amount,
status_tag = :status_tag, status_tag = :status_tag,
favorite = :favorite
favorite = :favorite,
tags = :tags
WHERE project_id=:project_id WHERE project_id=:project_id
`, &project)
`, toProjectDBO(project))
if err != nil { if err != nil {
return err return err
} }
@ -124,3 +140,55 @@ func (r *projectRepository) Delete(ctx context.Context, project models.Project)
return nil return nil
} }
type projectDBO struct {
ID string `json:"id" db:"project_id"`
UserID string `json:"-" db:"user_id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Icon string `json:"icon" db:"icon"`
Active bool `json:"active" db:"active"`
CreatedTime time.Time `json:"createdTime" db:"created_time"`
StartTime *time.Time `json:"startTime" db:"start_time"`
EndTime *time.Time `json:"endTime" db:"end_time"`
SubtractAmount int `json:"subtractAmount" db:"subtract_amount"`
StatusTag *string `json:"statusTag" db:"status_tag"`
Favorite bool `json:"favorite" db:"favorite"`
Tags pq.StringArray `json:"tags" db:"tags"`
}
func toProjectDBO(project models.Project) *projectDBO {
return &projectDBO{
ID: project.ID,
UserID: project.UserID,
Name: project.Name,
Description: project.Description,
Icon: project.Icon,
Active: project.Active,
CreatedTime: project.CreatedTime,
StartTime: project.StartTime,
EndTime: project.EndTime,
SubtractAmount: project.SubtractAmount,
StatusTag: project.StatusTag,
Favorite: project.Favorite,
Tags: project.Tags,
}
}
func (d *projectDBO) ToProject() *models.Project {
return &models.Project{
ID: d.ID,
UserID: d.UserID,
Name: d.Name,
Description: d.Description,
Icon: d.Icon,
Active: d.Active,
CreatedTime: d.CreatedTime,
StartTime: d.StartTime,
EndTime: d.EndTime,
SubtractAmount: d.SubtractAmount,
StatusTag: d.StatusTag,
Favorite: d.Favorite,
Tags: d.Tags,
}
}

11
migrations/postgres/20210418164739_add_project_column_tags.sql

@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project
ADD COLUMN tags TEXT[] DEFAULT ARRAY[]::TEXT[];
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE project
DROP COLUMN tags;
-- +goose StatementEnd

5
models/project.go

@ -18,6 +18,7 @@ type Project struct {
SubtractAmount int `json:"subtractAmount" db:"subtract_amount"` SubtractAmount int `json:"subtractAmount" db:"subtract_amount"`
StatusTag *string `json:"statusTag" db:"status_tag"` StatusTag *string `json:"statusTag" db:"status_tag"`
Favorite bool `json:"favorite" db:"favorite"` Favorite bool `json:"favorite" db:"favorite"`
Tags []string `json:"tags" db:"tags"`
} }
func (project *Project) Update(update ProjectUpdate) { func (project *Project) Update(update ProjectUpdate) {
@ -62,6 +63,9 @@ func (project *Project) Update(update ProjectUpdate) {
if update.Favorite != nil { if update.Favorite != nil {
project.Favorite = *update.Favorite project.Favorite = *update.Favorite
} }
if update.SetTags != nil {
project.Tags = update.SetTags
}
if project.StatusTag != nil && project.Active { if project.StatusTag != nil && project.Active {
project.StatusTag = nil project.StatusTag = nil
@ -80,6 +84,7 @@ type ProjectUpdate struct {
SubtractAmount *int `json:"subtractAmount"` SubtractAmount *int `json:"subtractAmount"`
StatusTag *string `json:"statusTag"` StatusTag *string `json:"statusTag"`
ClearStatusTag bool `json:"clearStatusTag"` ClearStatusTag bool `json:"clearStatusTag"`
SetTags []string `json:"setTags"`
Favorite *bool `json:"favorite"` Favorite *bool `json:"favorite"`
} }

30
models/projectgroup.go

@ -0,0 +1,30 @@
package models
import "context"
type ProjectGroup struct {
ID string `json:"id" db:"id"`
UserID string `json:"-" db:"user_id"`
Name string `json:"name" db:"name"`
Abbreviation string `json:"abbreviation" db:"abbreviation"`
CategoryNames map[string]string `json:"categoryNames" db:"category_names"`
}
type ProjectGroupUpdate struct {
Name *string `json:"name"`
Abbreviation *string `json:"abbreviation"`
SetCategoryNames map[string]string `json:"setCategoryNames"`
ResetCategoryName *string `json:"resetCategoryName"`
}
type ProjectGroupFilter struct {
UserID string `json:"userId"`
}
type ProjectGroupRepository interface {
Find(ctx context.Context, id string) (*ProjectGroup, error)
List(ctx context.Context, filter ProjectGroupFilter) ([]*ProjectGroup, error)
Insert(ctx context.Context, group ProjectGroup) error
Update(ctx context.Context, group ProjectGroup) error
Delete(ctx context.Context, group ProjectGroup) error
}

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

@ -92,6 +92,7 @@ import TimeProgress from "./TimeProgress.svelte";
<TimeProgress startTime={entry.startTime || entry.createdTime} endTime={entry.endTime} /> <TimeProgress startTime={entry.startTime || entry.createdTime} endTime={entry.endTime} />
{/if} {/if}
{/if} {/if}
<slot name="above-description"></slot>
{#if (full)} {#if (full)}
<Markdown source={entry.description} /> <Markdown source={entry.description} />
{/if} {/if}

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

@ -12,6 +12,7 @@ import stuffLogClient from "../clients/stufflog";
import ParentEntry from "./ParentEntry.svelte"; import ParentEntry from "./ParentEntry.svelte";
import ProgressNumbers from "./ProgressNumbers.svelte"; import ProgressNumbers from "./ProgressNumbers.svelte";
import StatusColor from "./StatusColor.svelte"; import StatusColor from "./StatusColor.svelte";
import TagList from "./TagList.svelte";
import TaskList from "./TaskList.svelte"; import TaskList from "./TaskList.svelte";
export let project: ProjectResult = null; export let project: ProjectResult = null;
@ -113,6 +114,9 @@ import stuffLogClient from "../clients/stufflog";
<div slot="post-seprator"> <div slot="post-seprator">
<ProgressNumbers project={project} /> <ProgressNumbers project={project} />
</div> </div>
<div slot="above-description">
<TagList tags={project.tags} />
</div>
{#if showAllOptions} {#if showAllOptions}
<OptionRow> <OptionRow>
<Option open={mdAddTask}>Add Task</Option> <Option open={mdAddTask}>Add Task</Option>

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

@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { ProjectResult } from "../models/project";
import App from "../App.svelte";
import type { ProjectResult } from "../models/project";
import selectionStore from "../stores/selection"; import selectionStore from "../stores/selection";
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 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 TimeProgress from "./TimeProgress.svelte"; import TimeProgress from "./TimeProgress.svelte";
export let project: ProjectResult; export let project: ProjectResult;
@ -30,6 +32,9 @@
</div> </div>
<div class="header"> <div class="header">
<div class="name"> <div class="name">
{#if project.tags.length > 0}
<Tag small value={project.tags[0]} />
{/if}
<div class="content">{project.name}</div> <div class="content">{project.name}</div>
{#if project.endTime} {#if project.endTime}
<div class="times"> <div class="times">

33
svelte-ui/src/components/Tag.svelte

@ -0,0 +1,33 @@
<script lang="ts">
export let value = "";
export let small = false;
let style = "";
$: {
let hash = 0;
for (let i = 0; i < value.length; i++) {
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)}%)`
}
</script>
<div style={style} class="tag" class:small>{value}</div>
<style>
div.tag {
padding: 0.25em 0.75ch;
margin: 0.25em 0.5ch;
font-size: 0.75em;
border: 1px solid;
}
div.tag.small {
padding: 0em 0.5ch;
}
</style>

18
svelte-ui/src/components/TagList.svelte

@ -0,0 +1,18 @@
<script lang="ts">
import Tag from "./Tag.svelte";
export let tags: string[] = [];
</script>
<div class="tag-list">
{#each tags as tag (tag)}
<Tag value={tag} />
{/each}
</div>
<style>
div.tag-list {
display: flex;
margin-top: 0.15em;
}
</style>

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

@ -27,6 +27,7 @@
tasks: [], tasks: [],
favorite: false, favorite: false,
subtractAmount: 0, subtractAmount: 0,
tags: [],
} }
let verb = "Add"; let verb = "Add";
if (md.name === "project.edit" || md.name === "project.delete") { if (md.name === "project.edit" || md.name === "project.delete") {
@ -46,6 +47,7 @@
let subtractAmount = project.subtractAmount; let subtractAmount = project.subtractAmount;
let error = null; let error = null;
let loading = false; let loading = false;
let tags = project.tags.join(", ");
function onSubmit() { function onSubmit() {
loading = true; loading = true;
@ -60,6 +62,7 @@
endTime: ( endTime == "" ) ? null : new Date(endTime), endTime: ( endTime == "" ) ? null : new Date(endTime),
statusTag: statusTag !== "" ? statusTag : null, statusTag: statusTag !== "" ? statusTag : null,
subtractAmount: Math.min(subtractAmount, 0), subtractAmount: Math.min(subtractAmount, 0),
tags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
name, description, icon, favorite, name, description, icon, favorite,
}).then(() => { }).then(() => {
@ -89,6 +92,7 @@
statusTag: statusTag || null, statusTag: statusTag || null,
clearStatusTag: statusTag === "", clearStatusTag: statusTag === "",
subtractAmount: subtractAmount, subtractAmount: subtractAmount,
setTags: tags.length > 0 ? tags.split(",").map(t => t.trim()) : [],
name, description, icon, favorite, name, description, icon, favorite,
}).then(() => { }).then(() => {
@ -128,6 +132,8 @@
{/if} {/if}
<label for="statusTag">Status</label> <label for="statusTag">Status</label>
<StatusTagSelect disabled={deletion} isProject bind:value={statusTag} /> <StatusTagSelect disabled={deletion} isProject bind:value={statusTag} />
<label for="tags">Tags (Comma Separated)</label>
<input disabled={deletion} name="tags" type="text" bind:value={tags} />
<Checkbox disabled={deletion} bind:checked={favorite} label="Mark as favorite." /> <Checkbox disabled={deletion} bind:checked={favorite} label="Mark as favorite." />

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

@ -10,6 +10,7 @@ export default interface Project {
createdTime: string createdTime: string
favorite: boolean favorite: boolean
subtractAmount: number subtractAmount: number
tags: string[]
startTime?: string startTime?: string
endTime?: string endTime?: string
statusTag?: string statusTag?: string
@ -36,6 +37,7 @@ export interface ProjectInput {
endTime?: string | Date endTime?: string | Date
statusTag?: string statusTag?: string
favorite?: boolean favorite?: boolean
tags?: string[]
} }
export interface ProjectUpdate { export interface ProjectUpdate {
@ -51,4 +53,5 @@ export interface ProjectUpdate {
clearStatusTag?: boolean clearStatusTag?: boolean
subtractAmount?: number subtractAmount?: number
favorite?: boolean favorite?: boolean
setTags?: string[]
} }
Loading…
Cancel
Save