Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf61cf39c2 | ||
|
|
d7661adc09 | ||
|
|
b2effd918d | ||
|
|
9e6bdb5fd4 | ||
|
|
2aa5919615 | ||
|
|
4e5a525219 | ||
|
|
ba3e9606e3 | ||
|
|
63674efde4 | ||
|
|
9ea7cf8ca4 | ||
|
|
fefb1b5e46 | ||
|
|
e983e68ed6 | ||
|
|
0ed233fb8a | ||
|
|
80e18c3c8a | ||
|
|
d25e24ed8a | ||
|
|
fe7a98b277 | ||
|
|
1f1fdcd34e | ||
|
|
14f3f7dc49 | ||
|
|
98175e4160 | ||
|
|
158386d6fd | ||
|
|
995504d969 | ||
|
|
97ad87680a | ||
|
|
c565ca5ea7 | ||
|
|
30bc93f9ee | ||
|
|
926723a12e | ||
|
|
78361977f2 | ||
|
|
9ac4a89af7 | ||
|
|
be8631feac | ||
|
|
9238e216ba | ||
|
|
d88352cc24 | ||
|
|
7276b026f5 | ||
|
|
16ec7b9bb5 | ||
|
|
b4fb329247 | ||
|
|
2f4a85f0a9 | ||
|
|
614687b0a9 | ||
|
|
01d8e8f014 | ||
|
|
fa91b4df12 | ||
|
|
b322383d3d | ||
|
|
a242cc4ee4 | ||
|
|
8cf56a4349 | ||
|
|
27dea52fec | ||
|
|
8c951be14d | ||
|
|
acaaca1b48 | ||
|
|
7064e9c693 | ||
|
|
a2baad731b | ||
|
|
9edf074ebd | ||
|
|
b716c79434 | ||
|
|
20b23ae822 | ||
|
|
a328184ad1 | ||
|
|
26aaa5ac3c | ||
|
|
d17abd20d1 | ||
|
|
900769db9f | ||
|
|
9f8ea798a6 | ||
|
|
b95a43abef | ||
|
|
c467448737 | ||
|
|
16ffae271d | ||
|
|
8e09378644 | ||
|
|
44685d9aa9 | ||
|
|
82fef3eea9 | ||
|
|
f6eddd4fbb | ||
|
|
539552a4ca | ||
|
|
4f4f67efa9 | ||
|
|
12e3f8053b | ||
|
|
d1da0eeafa | ||
|
|
8cd2412b25 | ||
|
|
e11a230921 | ||
|
|
d6d3877805 | ||
|
|
f57149c0d0 | ||
|
|
cbfbd7c3e7 | ||
|
|
344f743dc6 | ||
|
|
1b73c83b3b | ||
|
|
3a1142a965 | ||
|
|
3737a80c46 | ||
|
|
0d2e027816 | ||
|
|
cac4ac0bf6 | ||
|
|
f5dee09e40 | ||
|
|
53be464d7e | ||
|
|
b381e2cbed | ||
|
|
d7c1fbb21c | ||
|
|
d4141f5eb3 | ||
|
|
1232721921 | ||
|
|
a70bb4bb23 | ||
|
|
0ba65038e9 | ||
|
|
6c3fdad79c | ||
|
|
e307ed6021 | ||
|
|
e4c41e1d47 | ||
|
|
ae47f1a0e5 | ||
|
|
b9ffe256e9 | ||
|
|
eff677cc69 | ||
|
|
702b2254c8 | ||
|
|
972ad748f2 | ||
|
|
2f4c03b9af | ||
|
|
0bbfe3689e | ||
|
|
a93ff5b292 | ||
|
|
ae408390a5 | ||
|
|
5987b7c8e1 | ||
|
|
e268c1fd97 | ||
|
|
7b7a46eae9 | ||
|
|
6825584b8d | ||
|
|
a530bed697 | ||
|
|
e65349662a | ||
|
|
fd5d732851 | ||
|
|
9259609efe | ||
|
|
a5b9f85eda | ||
|
|
c503b7a30e | ||
|
|
b089933188 | ||
|
|
2bd1e0a9e3 | ||
|
|
43b75f77c6 | ||
|
|
c8ca2d4344 | ||
|
|
6641d99d41 | ||
|
|
396b87eea1 | ||
|
|
1d59182d43 | ||
|
|
951be980c0 | ||
|
|
3a8bb9709f | ||
|
|
015c0c18b9 | ||
|
|
ed828b0227 | ||
|
|
d4ab0a8555 | ||
|
|
22885734c7 | ||
|
|
795d49a942 | ||
|
|
d083c9b1e8 | ||
|
|
0e9b1a4930 | ||
|
|
6bbbf33c83 | ||
|
|
ad53ec5c7e | ||
|
|
1195f6ab03 | ||
|
|
2bd68930fe | ||
|
|
0ef4e38b8a | ||
|
|
48b5c17166 | ||
|
|
2fcb6a5d1a | ||
|
|
0dfda26c09 | ||
|
|
701932546d | ||
|
|
287b18e2e1 | ||
|
|
75dd79127e | ||
|
|
da6cdfdb0d | ||
|
|
7eb5f07299 | ||
|
|
d114f0d38c | ||
|
|
a3336b183c |
+107
-31
@@ -4,38 +4,114 @@ kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: go-test
|
||||
image: golang:1.18
|
||||
- name: backend-build
|
||||
image: golang:1.19
|
||||
depends_on: []
|
||||
commands:
|
||||
- go mod download
|
||||
- CGO_ENABLED=0 go build -ldflags "-w -s" -o build/handler cmd/stufflog3/main.go
|
||||
|
||||
- name: backend-test
|
||||
image: golang:1.19
|
||||
depends_on: []
|
||||
commands:
|
||||
- go generate ./...
|
||||
- go test -v ./...
|
||||
|
||||
- name: docker-backend-tag
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: true
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/stufflog3/backend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- name: backend-migrate
|
||||
image: golang:1.19
|
||||
depends_on: []
|
||||
environment:
|
||||
DB_CONNECT:
|
||||
from_secret: DB_CONNECT
|
||||
commands:
|
||||
- go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
- cd scripts/goose-mysql/
|
||||
- goose mysql "$DB_CONNECT" up
|
||||
|
||||
- name: docker-frontend-tag
|
||||
image: plugins/docker
|
||||
settings:
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
context: ./frontend
|
||||
auto_tag: true
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/stufflog3/frontend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- name: backend-deploy
|
||||
image: node:18.3.0
|
||||
depends_on:
|
||||
- backend-build
|
||||
- backend-test
|
||||
- backend-migrate
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: AWS_SECRET_ACCESS_KEY
|
||||
AWS_DEFAULT_REGION:
|
||||
from_secret: STUFFLOG3_AWS_REGION
|
||||
STUFFLOG3_AWS_CLIENT_ID:
|
||||
from_secret: STUFFLOG3_AWS_CLIENT_ID
|
||||
STUFFLOG3_AWS_CLIENT_SECRET:
|
||||
from_secret: STUFFLOG3_AWS_CLIENT_SECRET
|
||||
STUFFLOG3_AWS_POOL_CLIENT_ID:
|
||||
from_secret: STUFFLOG3_AWS_POOL_CLIENT_ID
|
||||
STUFFLOG3_AWS_POOL_CLIENT_SECRET:
|
||||
from_secret: STUFFLOG3_AWS_POOL_CLIENT_SECRET
|
||||
STUFFLOG3_AWS_POOL_ID:
|
||||
from_secret: STUFFLOG3_AWS_POOL_ID
|
||||
STUFFLOG3_AWS_REGION:
|
||||
from_secret: STUFFLOG3_AWS_REGION
|
||||
STUFFLOG3_MYSQL_HOST:
|
||||
from_secret: STUFFLOG3_MYSQL_HOST
|
||||
STUFFLOG3_MYSQL_PASSWORD:
|
||||
from_secret: STUFFLOG3_MYSQL_PASSWORD
|
||||
STUFFLOG3_MYSQL_PORT:
|
||||
from_secret: STUFFLOG3_MYSQL_PORT
|
||||
STUFFLOG3_MYSQL_SCHEMA:
|
||||
from_secret: STUFFLOG3_MYSQL_SCHEMA
|
||||
STUFFLOG3_MYSQL_USERNAME:
|
||||
from_secret: STUFFLOG3_MYSQL_USERNAME
|
||||
DOMAIN_NAME:
|
||||
from_secret: DOMAIN_NAME
|
||||
CERTIFICATE_ARN:
|
||||
from_secret: CERTIFICATE_ARN
|
||||
CERTIFICATE_NAME:
|
||||
from_secret: CERTIFICATE_NAME
|
||||
HOSTED_ZONE_ID:
|
||||
from_secret: HOSTED_ZONE_ID
|
||||
S3_WEBUI_BUCKET:
|
||||
from_secret: S3_WEBUI_BUCKET
|
||||
IAM_ROLE:
|
||||
from_secret: IAM_ROLE
|
||||
commands:
|
||||
- apt-get update > /dev/null 2>&1
|
||||
- apt-get -y install awscli zip > /dev/null 2>&1
|
||||
- npm install -g serverless > /dev/null 2>&1
|
||||
- npm install -g serverless-domain-manager > /dev/null 2>&1
|
||||
- npm install -g serverless-apigateway-service-proxy > /dev/null 2>&1
|
||||
- serverless deploy
|
||||
|
||||
- name: frontend-build
|
||||
image: node:18.3.0
|
||||
depends_on: []
|
||||
environment:
|
||||
VITE_STUFFLOG3_AWS_POOL_REGION:
|
||||
from_secret: VITE_STUFFLOG3_AWS_POOL_REGION
|
||||
VITE_STUFFLOG3_AWS_POOL_ID:
|
||||
from_secret: VITE_STUFFLOG3_AWS_POOL_ID
|
||||
VITE_STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID:
|
||||
from_secret: VITE_STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID
|
||||
commands:
|
||||
- cd frontend
|
||||
- npm install
|
||||
- npm run build
|
||||
- cp static/* build/
|
||||
|
||||
- name: frontend-deploy
|
||||
image: amazon/aws-cli:latest
|
||||
depends_on:
|
||||
- frontend-build
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: AWS_SECRET_ACCESS_KEY
|
||||
AWS_DEFAULT_REGION:
|
||||
from_secret: STUFFLOG3_AWS_REGION
|
||||
S3_WEBUI_BUCKET:
|
||||
from_secret: S3_WEBUI_BUCKET
|
||||
commands:
|
||||
- cd frontend/build
|
||||
- aws s3 sync --delete . s3://$S3_WEBUI_BUCKET
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ COPY . .
|
||||
|
||||
# Build server and tools (CGO disabled to make them 100% static)
|
||||
ENV CGO_ENABLED 0
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/stufflog3-local ./cmd/stufflog3-local
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/stufflog3 ./cmd/stufflog3
|
||||
|
||||
## 2. Distribute
|
||||
# Use alpine linux
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/cognitoauth"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/httpapi"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/mysql"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/auth"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := mysql.Connect(
|
||||
os.Getenv("STUFFLOG3_MYSQL_HOST"),
|
||||
envInt("STUFFLOG3_MYSQL_PORT"),
|
||||
os.Getenv("STUFFLOG3_MYSQL_USERNAME"),
|
||||
os.Getenv("STUFFLOG3_MYSQL_PASSWORD"),
|
||||
os.Getenv("STUFFLOG3_MYSQL_SCHEMA"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("Failed to open database:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cognitoAuth, err := cognitoauth.New(
|
||||
os.Getenv("STUFFLOG3_AWS_REGION"),
|
||||
os.Getenv("STUFFLOG3_AWS_CLIENT_ID"),
|
||||
os.Getenv("STUFFLOG3_AWS_CLIENT_SECRET"),
|
||||
os.Getenv("STUFFLOG3_AWS_POOL_ID"),
|
||||
os.Getenv("STUFFLOG3_AWS_POOL_CLIENT_ID"),
|
||||
os.Getenv("STUFFLOG3_AWS_POOL_CLIENT_SECRET"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("Failed to setup cognito:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
authService := &auth.Service{
|
||||
Provider: cognitoAuth,
|
||||
}
|
||||
scopesService := &scopes.Service{
|
||||
Auth: authService,
|
||||
Repository: db.Scopes(),
|
||||
StatsLister: db.Stats(),
|
||||
}
|
||||
statsService := &stats.Service{
|
||||
Scopes: scopesService,
|
||||
Repository: db.Stats(),
|
||||
}
|
||||
itemsService := &items.Service{
|
||||
Auth: authService,
|
||||
Scopes: scopesService,
|
||||
Stats: statsService,
|
||||
Repository: db.Items(),
|
||||
RequirementFetcher: db.Projects(),
|
||||
}
|
||||
projectsService := &projects.Service{
|
||||
Auth: authService,
|
||||
Scopes: scopesService,
|
||||
Stats: statsService,
|
||||
Items: itemsService,
|
||||
Repository: db.Projects(),
|
||||
}
|
||||
sprintsService := &sprints.Service{
|
||||
Scopes: scopesService,
|
||||
Items: itemsService,
|
||||
Projects: projectsService,
|
||||
Repository: db.Sprints(),
|
||||
}
|
||||
|
||||
server := gin.New()
|
||||
httpapi.Auth(server.Group("/api/v1/auth"), authService)
|
||||
apiV1 := server.Group("/api/v1")
|
||||
if os.Getenv("STUFFLOG3_USE_DUMMY_USER") == "true" {
|
||||
log.Println("Using dummy UUID")
|
||||
apiV1.Use(httpapi.DummyMiddleware(authService, "c11230be-4912-4313-83b0-410a248b5bd1"))
|
||||
} else {
|
||||
apiV1.Use(httpapi.JwtValidatorMiddleware(authService))
|
||||
}
|
||||
apiV1Scopes := apiV1.Group("/scopes")
|
||||
httpapi.Scopes(apiV1Scopes, scopesService)
|
||||
apiV1ScopesSub := apiV1Scopes.Group("/:scope_id")
|
||||
apiV1ScopesSub.Use(httpapi.ScopeMiddleware(scopesService, authService))
|
||||
httpapi.Projects(apiV1ScopesSub.Group("/projects"), projectsService)
|
||||
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
|
||||
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
|
||||
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
|
||||
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
err := server.Run(":8239")
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case sig := <-exitSignal:
|
||||
{
|
||||
log.Println("Received signal", sig)
|
||||
os.Exit(0)
|
||||
}
|
||||
case err := <-errCh:
|
||||
{
|
||||
log.Println("Server run failed:", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func envInt(key string) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return intValue
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/cognitoauth"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/httpapi"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/mysql"
|
||||
@@ -17,10 +18,16 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var flagLocal = flag.Bool("local", false, "If set, run locally")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
db, err := mysql.Connect(
|
||||
os.Getenv("STUFFLOG3_MYSQL_HOST"),
|
||||
envInt("STUFFLOG3_MYSQL_PORT"),
|
||||
@@ -64,6 +71,7 @@ func main() {
|
||||
Stats: statsService,
|
||||
Repository: db.Items(),
|
||||
RequirementFetcher: db.Projects(),
|
||||
ProjectFetcher: db.Projects(),
|
||||
}
|
||||
projectsService := &projects.Service{
|
||||
Auth: authService,
|
||||
@@ -93,14 +101,46 @@ func main() {
|
||||
apiV1ScopesSub := apiV1Scopes.Group("/:scope_id")
|
||||
apiV1ScopesSub.Use(httpapi.ScopeMiddleware(scopesService, authService))
|
||||
httpapi.Projects(apiV1ScopesSub.Group("/projects"), projectsService)
|
||||
httpapi.Requirements(apiV1ScopesSub.Group("/requirements"), projectsService)
|
||||
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
|
||||
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
|
||||
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
|
||||
apiV1AllScopes := apiV1.Group("/all-scopes")
|
||||
apiV1AllScopes.Use(httpapi.AllScopeMiddleware(scopesService, authService))
|
||||
httpapi.ItemsAllScopes(apiV1AllScopes.Group("/items"), itemsService)
|
||||
httpapi.SprintsAllScopes(apiV1AllScopes.Group("/sprints"), sprintsService)
|
||||
httpapi.RequirementsAllScopes(apiV1AllScopes.Group("/requirements"), projectsService)
|
||||
|
||||
ginLambda := ginadapter.New(server)
|
||||
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||
return ginLambda.ProxyWithContext(ctx, request)
|
||||
})
|
||||
if *flagLocal {
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
err := server.Run(":8239")
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case sig := <-exitSignal:
|
||||
{
|
||||
log.Println("Received signal", sig)
|
||||
os.Exit(0)
|
||||
}
|
||||
case err := <-errCh:
|
||||
{
|
||||
log.Println("Server run failed:", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ginLambda := ginadapter.New(server)
|
||||
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||
return ginLambda.ProxyWithContext(ctx, request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func envInt(key string) int {
|
||||
@@ -1,7 +1,10 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +16,7 @@ type Item struct {
|
||||
RequirementID *int `json:"requirementId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedTime time.Time `json:"createdTime"`
|
||||
AcquiredTime *time.Time `json:"acquiredTime"`
|
||||
ScheduledDate *models.Date `json:"scheduledDate"`
|
||||
@@ -25,6 +29,24 @@ type ItemStat struct {
|
||||
Required int `json:"required"`
|
||||
}
|
||||
|
||||
func (item *Item) AcquiredOutside(fromTime, toTime time.Time) bool {
|
||||
return item.AcquiredTime != nil && (item.AcquiredTime.Before(fromTime) || !item.AcquiredTime.Before(toTime))
|
||||
}
|
||||
|
||||
func (item *Item) AcquiredBetween(fromTime, toTime time.Time) bool {
|
||||
return item.AcquiredTime != nil && !item.AcquiredTime.Before(fromTime) && item.AcquiredTime.Before(toTime)
|
||||
}
|
||||
|
||||
func (item *Item) HasTag(tag string) bool {
|
||||
for _, tag2 := range item.Tags {
|
||||
if strings.EqualFold(tag, tag2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (item *Item) ApplyUpdate(update models.ItemUpdate) {
|
||||
if update.RequirementID != nil {
|
||||
if *update.RequirementID <= 0 {
|
||||
@@ -54,4 +76,10 @@ func (item *Item) ApplyUpdate(update models.ItemUpdate) {
|
||||
if update.ClearAcquiredTime {
|
||||
item.AcquiredTime = nil
|
||||
}
|
||||
|
||||
if update.RemoveTags != nil || update.AddTags != nil {
|
||||
item.Tags = genutils.SliceWithout(item.Tags, update.RemoveTags)
|
||||
item.Tags = genutils.SliceWithUniques(item.Tags, update.AddTags)
|
||||
sort.Strings(item.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
+33
-6
@@ -1,7 +1,9 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +15,7 @@ type Project struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status models.Status `json:"status"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (project *Project) Update(update models.ProjectUpdate) {
|
||||
@@ -28,15 +31,24 @@ func (project *Project) Update(update models.ProjectUpdate) {
|
||||
if update.Status != nil {
|
||||
project.Status = *update.Status
|
||||
}
|
||||
|
||||
if update.RemoveTags != nil || update.AddTags != nil {
|
||||
project.Tags = genutils.SliceWithout(project.Tags, update.RemoveTags)
|
||||
project.Tags = genutils.SliceWithUniques(project.Tags, update.AddTags)
|
||||
sort.Strings(project.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
type Requirement struct {
|
||||
ID int `json:"id"`
|
||||
ScopeID int `json:"scopeId"`
|
||||
ProjectID int `json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status models.Status `json:"status"`
|
||||
ID int `json:"id"`
|
||||
ScopeID int `json:"scopeId"`
|
||||
ProjectID int `json:"projectId"`
|
||||
AggregateRequired int `json:"aggregateRequired"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsCoarse bool `json:"isCoarse"`
|
||||
Status models.Status `json:"status"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
||||
@@ -49,6 +61,21 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
|
||||
if update.Status != nil && update.Status.Valid() {
|
||||
requirement.Status = *update.Status
|
||||
}
|
||||
if update.IsCoarse != nil {
|
||||
requirement.IsCoarse = *update.IsCoarse
|
||||
}
|
||||
if update.AggregateRequired != nil {
|
||||
requirement.AggregateRequired = *update.AggregateRequired
|
||||
}
|
||||
if update.ProjectID != nil {
|
||||
requirement.ProjectID = *update.ProjectID
|
||||
}
|
||||
|
||||
if update.RemoveTags != nil || update.AddTags != nil {
|
||||
requirement.Tags = genutils.SliceWithout(requirement.Tags, update.RemoveTags)
|
||||
requirement.Tags = genutils.SliceWithUniques(requirement.Tags, update.AddTags)
|
||||
sort.Strings(requirement.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
type RequirementStat struct {
|
||||
|
||||
@@ -12,6 +12,8 @@ type Sprint struct {
|
||||
Description string `json:"description"`
|
||||
Kind models.SprintKind `json:"kind"`
|
||||
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
FromTime time.Time `json:"fromTime"`
|
||||
ToTime time.Time `json:"toTime"`
|
||||
|
||||
@@ -58,4 +60,11 @@ func (sprint *Sprint) ApplyUpdate(update models.SprintUpdate) {
|
||||
if update.AggregateRequired != nil {
|
||||
sprint.AggregateRequired = *update.AggregateRequired
|
||||
}
|
||||
if update.Tags != nil {
|
||||
if len(update.Tags) == 0 {
|
||||
sprint.Tags = nil
|
||||
} else {
|
||||
sprint.Tags = update.Tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+190
-161
@@ -10,11 +10,11 @@
|
||||
"devDependencies": {
|
||||
"@aws-amplify/auth": "^4.5.6",
|
||||
"@aws-amplify/core": "^4.5.6",
|
||||
"@djverrall/sveltekit-lambda-adapter": "^0.0.14",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@nikso/adapter-serverless": "^0.1.0",
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.78",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.43",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/marked": "^4.0.3",
|
||||
@@ -23,6 +23,7 @@
|
||||
"cookie": "^0.5.0",
|
||||
"fa-svelte": "^3.1.0",
|
||||
"insane": "^2.6.2",
|
||||
"layercake": "^7.1.0",
|
||||
"marked": "^4.0.17",
|
||||
"node-sass": "^7.0.1",
|
||||
"sass": "^1.52.3",
|
||||
@@ -7224,34 +7225,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@djverrall/sveltekit-lambda-adapter": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@djverrall/sveltekit-lambda-adapter/-/sveltekit-lambda-adapter-0.0.14.tgz",
|
||||
"integrity": "sha512-kjg3du9tS9aFAoLMiD/MH36exky9tg27zw1aG8VUY3M5xL6g0nbstULPBZYwSkVFcBXupVLbBkWihFXCzNqxgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-types": "^2.1.35",
|
||||
"node-fetch": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@djverrall/sveltekit-lambda-adapter/node_modules/node-fetch": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.6.tgz",
|
||||
"integrity": "sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz",
|
||||
@@ -8494,6 +8467,12 @@
|
||||
"tiny-glob": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "1.0.0-next.43",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-1.0.0-next.43.tgz",
|
||||
"integrity": "sha512-PAgSp1GA8HkYE4p30/sBvJme2nefhcTBJafqQdMNoUksWZF2WzuL8OEO8wa9ndE6cghcGk3j6Ve0Oskg/wtTOw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-vercel": {
|
||||
"version": "1.0.0-next.58",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-vercel/-/adapter-vercel-1.0.0-next.58.tgz",
|
||||
@@ -11042,6 +11021,88 @@
|
||||
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
|
||||
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
|
||||
"integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@@ -11054,15 +11115,6 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
|
||||
@@ -12733,29 +12785,6 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz",
|
||||
"integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -13014,18 +13043,6 @@
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
|
||||
@@ -14131,6 +14148,15 @@
|
||||
"he": "0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
@@ -15137,6 +15163,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/layercake": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/layercake/-/layercake-7.1.0.tgz",
|
||||
"integrity": "sha512-DV6FsmG0c5a2UyB15QBKBhgxhUW7E1615UTiTQGs2cnfSHPdgo8PfhAdX/E6AZwYhN9oLECpINBhEg3WNlIKjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"d3-scale": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
@@ -16721,25 +16756,6 @@
|
||||
"node": ">= 0.10.5"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
@@ -21633,15 +21649,6 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@@ -28179,29 +28186,6 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@djverrall/sveltekit-lambda-adapter": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@djverrall/sveltekit-lambda-adapter/-/sveltekit-lambda-adapter-0.0.14.tgz",
|
||||
"integrity": "sha512-kjg3du9tS9aFAoLMiD/MH36exky9tg27zw1aG8VUY3M5xL6g0nbstULPBZYwSkVFcBXupVLbBkWihFXCzNqxgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-types": "^2.1.35",
|
||||
"node-fetch": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.6.tgz",
|
||||
"integrity": "sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz",
|
||||
@@ -29231,6 +29215,12 @@
|
||||
"tiny-glob": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"@sveltejs/adapter-static": {
|
||||
"version": "1.0.0-next.43",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-1.0.0-next.43.tgz",
|
||||
"integrity": "sha512-PAgSp1GA8HkYE4p30/sBvJme2nefhcTBJafqQdMNoUksWZF2WzuL8OEO8wa9ndE6cghcGk3j6Ve0Oskg/wtTOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@sveltejs/adapter-vercel": {
|
||||
"version": "1.0.0-next.58",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-vercel/-/adapter-vercel-1.0.0-next.58.tgz",
|
||||
@@ -31318,6 +31308,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz",
|
||||
"integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"internmap": "1 - 2"
|
||||
}
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"dev": true
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"dev": true
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"d3-color": "1 - 3"
|
||||
}
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
}
|
||||
},
|
||||
"d3-time": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
|
||||
"integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"d3-array": "2 - 3"
|
||||
}
|
||||
},
|
||||
"d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"d3-time": "1 - 3"
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@@ -31327,12 +31378,6 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"data-uri-to-buffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
|
||||
"dev": true
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
|
||||
@@ -32591,16 +32636,6 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"fetch-blob": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz",
|
||||
"integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -32789,15 +32824,6 @@
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
|
||||
@@ -33660,6 +33686,12 @@
|
||||
"he": "0.5.0"
|
||||
}
|
||||
},
|
||||
"internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"dev": true
|
||||
},
|
||||
"invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
@@ -34486,6 +34518,15 @@
|
||||
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
|
||||
"dev": true
|
||||
},
|
||||
"layercake": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/layercake/-/layercake-7.1.0.tgz",
|
||||
"integrity": "sha512-DV6FsmG0c5a2UyB15QBKBhgxhUW7E1615UTiTQGs2cnfSHPdgo8PfhAdX/E6AZwYhN9oLECpINBhEg3WNlIKjw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"d3-scale": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
@@ -35822,12 +35863,6 @@
|
||||
"minimatch": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
@@ -39702,12 +39737,6 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
|
||||
"dev": true
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
"devDependencies": {
|
||||
"@aws-amplify/auth": "^4.5.6",
|
||||
"@aws-amplify/core": "^4.5.6",
|
||||
"@djverrall/sveltekit-lambda-adapter": "^0.0.14",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@nikso/adapter-serverless": "^0.1.0",
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.78",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.43",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/marked": "^4.0.3",
|
||||
@@ -26,6 +26,7 @@
|
||||
"cookie": "^0.5.0",
|
||||
"fa-svelte": "^3.1.0",
|
||||
"insane": "^2.6.2",
|
||||
"layercake": "^7.1.0",
|
||||
"marked": "^4.0.17",
|
||||
"node-sass": "^7.0.1",
|
||||
"sass": "^1.52.3",
|
||||
|
||||
Vendored
+3
-4
@@ -10,13 +10,12 @@ declare namespace App {
|
||||
}
|
||||
// interface Platform {}
|
||||
interface Session {
|
||||
user?: import("$lib/models/user").default
|
||||
idToken?: string
|
||||
req?: any
|
||||
amplifyConfig: any
|
||||
groupOptions?: import("$lib/models/items").GroupItemsOptions
|
||||
}
|
||||
|
||||
interface Stuff {
|
||||
idToken?: string,
|
||||
title?: string,
|
||||
scope?: import("$lib/models/scope").default
|
||||
}
|
||||
}
|
||||
|
||||
+2
-64
@@ -1,65 +1,3 @@
|
||||
import cookie from 'cookie';
|
||||
import { Amplify, withSSRContext } from 'aws-amplify';
|
||||
import type { GetSession, Handle } from '@sveltejs/kit';
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
region: process.env.STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: process.env.STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: process.env.STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID,
|
||||
},
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
|
||||
export const getSession: GetSession = ({ locals, request }) => {
|
||||
return {
|
||||
idToken: locals.idToken,
|
||||
user: locals.user,
|
||||
req: { headers: { cookie: request.headers.get("Cookie") } },
|
||||
amplifyConfig: {
|
||||
region: process.env.STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: process.env.STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: process.env.STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const handle: Handle = async({ event, resolve }) => {
|
||||
const {Auth} = withSSRContext({
|
||||
req: { headers: { cookie: event.request.headers.get("Cookie") } }
|
||||
});
|
||||
|
||||
const newCookies = {};
|
||||
|
||||
try {
|
||||
const curr = await Auth.currentAuthenticatedUser();
|
||||
event.locals.idToken = curr.signInUserSession?.idToken?.jwtToken;
|
||||
if (event.locals.idToken != null) {
|
||||
event.locals.user = {
|
||||
id: curr.signInUserSession.idToken.payload["sub"],
|
||||
name: curr.signInUserSession.idToken.payload["cognito:username"],
|
||||
}
|
||||
}
|
||||
|
||||
const store = curr?.storage?.store || {};
|
||||
const cookies = cookie.parse(event.request.headers.get("Cookie"));
|
||||
for (const key in store) {
|
||||
if (store[key] != cookies[key]) {
|
||||
newCookies[key] = store[key]
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.warn(err)
|
||||
}
|
||||
|
||||
const response = await resolve(event);
|
||||
|
||||
for (const key in newCookies) {
|
||||
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
|
||||
path: "/",
|
||||
}));
|
||||
}
|
||||
|
||||
return response;
|
||||
export async function handle({ event, resolve }) {
|
||||
return resolve(event, { ssr: false });
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@@ -1,8 +1,8 @@
|
||||
import type { TimeInterval } from "$lib/models/common";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter, ItemInput } from "$lib/models/item";
|
||||
import type { GroupItemsOptions, ItemFilter, ItemGroup, ItemInput } from "$lib/models/item";
|
||||
import type Project from "$lib/models/project";
|
||||
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project";
|
||||
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput, RequirementWithoutItems } from "$lib/models/project";
|
||||
import type { ScopeInput } from "$lib/models/scope";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import type { SprintInput, SprintInputPart } from "$lib/models/sprint";
|
||||
@@ -11,6 +11,8 @@ import type Stat from "$lib/models/stat";
|
||||
import type { StatInput } from "$lib/models/stat";
|
||||
import type User from "$lib/models/user";
|
||||
import type { AuthResult } from "$lib/models/user";
|
||||
import { Auth } from "aws-amplify";
|
||||
import { getContext, setContext } from "svelte";
|
||||
|
||||
type FetchImpl = (info: RequestInfo, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
@@ -25,7 +27,7 @@ export default class SL3APIClient {
|
||||
if (typeof(root) === "string") {
|
||||
this.root = root || "";
|
||||
} else if (root === true) {
|
||||
this.root = process.env.STUFFLOG3_API;
|
||||
this.root = import.meta.env.VITE_STUFFLOG3_API;
|
||||
} else {
|
||||
this.root = ""
|
||||
}
|
||||
@@ -53,6 +55,12 @@ export default class SL3APIClient {
|
||||
return this.fetch<{projects: ProjectEntry[]}>("GET", `scopes/${scopeId}/projects`).then(r => r.projects);
|
||||
}
|
||||
|
||||
async listRequirements(scopeId: number | null): Promise<RequirementWithoutItems[]> {
|
||||
const scopePrefix = scopeId !== null ? `scopes/${scopeId}` : `all-scopes`;
|
||||
|
||||
return this.fetch<{requirements: RequirementWithoutItems[]}>("GET", `${scopePrefix}/requirements`).then(r => r.requirements);
|
||||
}
|
||||
|
||||
async createRequirement(scopeId: number, projectId: number, input: RequirementInput): Promise<Requirement> {
|
||||
return this.fetch<{requirement: Requirement}>("POST", `scopes/${scopeId}/projects/${projectId}/requirements`, input).then(r => r.requirement);
|
||||
}
|
||||
@@ -73,8 +81,19 @@ export default class SL3APIClient {
|
||||
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
|
||||
}
|
||||
|
||||
async listItems(scopeId: number, filter: ItemFilter): Promise<Item[]> {
|
||||
return this.fetch<{items: Item[]}>("GET", `scopes/${scopeId}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
|
||||
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped: true, groupOptions: GroupItemsOptions): Promise<{items: Item[], groups: ItemGroup[]}>
|
||||
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: false): Promise<Item[]>
|
||||
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: boolean, groupOptions?: GroupItemsOptions): Promise<Item[] | {items: Item[], groups: ItemGroup[]}> {
|
||||
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
|
||||
const groupedSuffix = !!grouped ? `&grouped=true&hideCreated=${groupOptions?.hideCreated}&hideAcquired=${groupOptions?.hideAcquired}&hideScheduled=${groupOptions?.hideScheduled}` : "";
|
||||
|
||||
return this.fetch<{items: Item[], groups: ItemGroup[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}${groupedSuffix}`).then(r => {
|
||||
if (!grouped) {
|
||||
return r.items
|
||||
}
|
||||
|
||||
return {groups: r.groups, items: r.items}
|
||||
});
|
||||
}
|
||||
|
||||
async createItem(scopeId: number, input: ItemInput): Promise<Item> {
|
||||
@@ -97,15 +116,17 @@ export default class SL3APIClient {
|
||||
return this.fetch<{sprint: Sprint}>("GET", `scopes/${scopeId}/sprints/${sprintId}`).then(r => r.sprint);
|
||||
}
|
||||
|
||||
async listSprints(scopeId: number, interval?: TimeInterval<Date | string>): Promise<Sprint[]> {
|
||||
async listSprints(scopeId: number | "ALL", interval?: TimeInterval<Date | string>): Promise<Sprint[]> {
|
||||
let qs = "";
|
||||
if (interval != null) {
|
||||
const minStr = (interval.min instanceof Date) ? interval.min.toISOString() : interval.min;
|
||||
const maxStr = (interval.max instanceof Date) ? interval.max.toISOString() : interval.max;
|
||||
qs = `?from=${encodeURIComponent(minStr)}&to=${encodeURIComponent(maxStr)}`
|
||||
}
|
||||
|
||||
let prefix = (scopeId === "ALL") ? `all-scopes` : `scopes/${scopeId}`
|
||||
|
||||
return this.fetch<{sprints: Sprint[]}>("GET", `scopes/${scopeId}/sprints${qs}`).then(r => r.sprints);
|
||||
return this.fetch<{sprints: Sprint[]}>("GET", `${prefix}/sprints${qs}`).then(r => r.sprints);
|
||||
}
|
||||
|
||||
async createSprint(scopeId: number, input: SprintInput): Promise<Sprint> {
|
||||
@@ -145,9 +166,12 @@ export default class SL3APIClient {
|
||||
bodyData = JSON.stringify(body);
|
||||
}
|
||||
|
||||
if (this.tokenPromise != null) {
|
||||
headers["Authorization"] = `Bearer ${await this.tokenPromise}`;
|
||||
}
|
||||
try {
|
||||
const sess = await Auth.currentSession();
|
||||
const idToken = sess?.getIdToken()?.getJwtToken();
|
||||
|
||||
headers["Authorization"] = `Bearer ${idToken}`;
|
||||
} catch(_) {}
|
||||
|
||||
const res = await this.fetcher(`${this.root}/api/v1/${path}`, {
|
||||
method, headers,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<script lang="ts">
|
||||
import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
import { significantDecimals } from "$lib/utils/numbers";
|
||||
import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
|
||||
export let label: string
|
||||
export let value: number
|
||||
export let target: number = null
|
||||
</script>
|
||||
export let right: boolean = false;
|
||||
export let dark: boolean = false;
|
||||
export let weighted: boolean = false;
|
||||
|
||||
<div class="amount">
|
||||
<span class="value">{value}×</span>
|
||||
let valueString: string;
|
||||
$: valueString = !weighted ? Math.round(value).toFixed() : significantDecimals(value);
|
||||
|
||||
</script>
|
||||
|
||||
<div class="amount" class:right>
|
||||
<span class:dark class="value">{valueString}×</span>
|
||||
<span class="label">{label}</span>
|
||||
<span class="target">(<PlusMinus offset={value - target} />)</span>
|
||||
{ #if target != null && target != value }
|
||||
<span class="target">(<PlusMinus integersOnly={!weighted} offset={value - target} />)</span>
|
||||
{ /if }
|
||||
</div>
|
||||
@@ -19,9 +26,14 @@ import PlusMinus from "../layout/PlusMinus.svelte"
|
||||
@import "../../css/colors.sass";
|
||||
|
||||
div.amount {
|
||||
padding-right: 1ch;
|
||||
|
||||
&.right {
|
||||
padding-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
span.label {
|
||||
|
||||
span.label, span.dark {
|
||||
opacity: $opacity-entry4;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" context="module">
|
||||
const MILESTONE_COLORS = [
|
||||
"#f4b083",
|
||||
"#d8dce4",
|
||||
"#ffd966",
|
||||
"#8ef88e",
|
||||
"#84f5ff",
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { LayerCake, Svg } from 'layercake';
|
||||
import Line from './layercake/Line.svelte';
|
||||
import AxisY from './layercake/AxisY.svelte';
|
||||
|
||||
import type { SprintBurndownDataPoint } from '$lib/models/sprint';
|
||||
import { getTimeContext } from '../contexts/TimeContext.svelte';
|
||||
import AxisX from './layercake/AxisX.svelte';
|
||||
|
||||
const {now} = getTimeContext();
|
||||
|
||||
export let data: SprintBurndownDataPoint[]
|
||||
export let from: Date | string | number
|
||||
export let to: Date | string | number
|
||||
export let milestone: number = 10000;
|
||||
export let green: boolean = false;
|
||||
|
||||
let after = false;
|
||||
let today = 0;
|
||||
let lineColor = "#777777";
|
||||
let points: {x: number, y: number}[] = [];
|
||||
let milestones: number[] = [];
|
||||
let milestoneIndex = 0;
|
||||
let fromTime: number
|
||||
let toTime: number
|
||||
let timeSplits: number[] = [];
|
||||
|
||||
$: fromTime = Math.floor(new Date(from).getTime() / 86400000) * 86400000;
|
||||
$: toTime = Math.ceil(new Date(to).getTime() / 86400000) * 86400000;
|
||||
$: after = new Date(to).getTime() < $now.getTime();
|
||||
$: today = Math.floor($now.getTime() / 86400000) * 86400000;
|
||||
|
||||
$: {
|
||||
const dataMap = {}
|
||||
dataMap[fromTime] = 0;
|
||||
if (after) {
|
||||
dataMap[toTime] = data?.[data?.length - 1]?.value || 0;
|
||||
} else {
|
||||
dataMap[today] = data?.[data?.length - 1]?.value || 0;
|
||||
}
|
||||
|
||||
timeSplits = [];
|
||||
if (toTime - fromTime > (86400000 * 350)) {
|
||||
let current = new Date(fromTime);
|
||||
current.setUTCDate(1);
|
||||
while (current.getTime() < toTime) {
|
||||
timeSplits.push(current.getTime());
|
||||
current.setUTCMonth(current.getUTCMonth() + 1);
|
||||
}
|
||||
} else if (toTime - fromTime > (86400000 * 7)) {
|
||||
for (let x = fromTime; x < toTime; x += 86400000 * 7) {
|
||||
timeSplits.push(x)
|
||||
}
|
||||
} else {
|
||||
for (let x = fromTime; x < toTime; x += 86400000) {
|
||||
timeSplits.push(x)
|
||||
}
|
||||
}
|
||||
|
||||
let last = 0;
|
||||
for (const {date, value} of data) {
|
||||
const currentX = new Date(date+"T00:00:00Z").getTime();
|
||||
const prevX = currentX - 86400000;
|
||||
|
||||
if (dataMap[prevX] == null && prevX > fromTime) {
|
||||
dataMap[prevX] = last;
|
||||
}
|
||||
dataMap[currentX] = value;
|
||||
last = value;
|
||||
}
|
||||
|
||||
milestones = [];
|
||||
milestoneIndex = 0;
|
||||
for (let n = milestone; n < last; n += milestone) {
|
||||
milestones.push(n);
|
||||
milestoneIndex += 1;
|
||||
if (milestoneIndex == MILESTONE_COLORS.length - 1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (green) {
|
||||
lineColor = "#78ff78";
|
||||
} else {
|
||||
lineColor = MILESTONE_COLORS[milestoneIndex];
|
||||
}
|
||||
|
||||
points = Object.keys(dataMap).map(k => ({x: parseInt(k), y: dataMap[k]})).sort((a,b) => a.x - b.x);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="chart-container">
|
||||
<LayerCake
|
||||
padding={{ top: 0, right: 5, bottom: 0, left: 5 }}
|
||||
x="x"
|
||||
y="y"
|
||||
yNice={8}
|
||||
xDomain={[fromTime, toTime + (3600000*3)]}
|
||||
yDomain={[0, milestones.length == 0 ? milestone : null]}
|
||||
data={points}
|
||||
>
|
||||
<Svg>
|
||||
<AxisX
|
||||
ticks={timeSplits}
|
||||
formatTick={_t => ""}
|
||||
/>
|
||||
<AxisY
|
||||
ticks={milestones}
|
||||
formatTick={_tick => ""}
|
||||
gridlines={true}
|
||||
/>
|
||||
<Line stroke={lineColor} />
|
||||
</Svg>
|
||||
</LayerCake>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.chart-container
|
||||
width: 100%
|
||||
height: 30px
|
||||
</style>
|
||||
@@ -8,24 +8,26 @@
|
||||
<div class="par">(</div>
|
||||
<div class="selector">
|
||||
<select value={value} on:change>
|
||||
<option value="last:7d">Last 7 days</option>
|
||||
<option value="last:14d">Last 14 days</option>
|
||||
<option value="last:30d">Last 30 days</option>
|
||||
<option value="last:90d">Last 90 days</option>
|
||||
<option value="last:1m">Last 1 month</option>
|
||||
<option value="last:3m">Last 3 months</option>
|
||||
<option value="last:6m">Last 6 months</option>
|
||||
<option value="last:1y">Last 1 year</option>
|
||||
<option value="this_week">This week</option>
|
||||
<option value="next_week">Next week</option>
|
||||
<option value="last_week">Last week</option>
|
||||
<option value="this_month">This month</option>
|
||||
<option value="next_month">Next month</option>
|
||||
<option value="last_month">Last month</option>
|
||||
<option value="all_time">All time</option>
|
||||
<option selected={value === "last:7d" } value="last:7d">Last 7 days</option>
|
||||
<option selected={value === "last:14d" } value="last:14d">Last 14 days</option>
|
||||
<option selected={value === "last:30d" } value="last:30d">Last 30 days</option>
|
||||
<option selected={value === "last:90d" } value="last:90d">Last 90 days</option>
|
||||
<option selected={value === "last:1m" } value="last:1m">Last 1 month</option>
|
||||
<option selected={value === "last:3m" } value="last:3m">Last 3 months</option>
|
||||
<option selected={value === "last:6m" } value="last:6m">Last 6 months</option>
|
||||
<option selected={value === "last:1y" } value="last:1y">Last 1 year</option>
|
||||
<option selected={value === "this_week" } value="this_week">This week</option>
|
||||
<option selected={value === "next_week" } value="next_week">Next week</option>
|
||||
<option selected={value === "last_week" } value="last_week">Last week</option>
|
||||
<option selected={value === "this_month" } value="this_month">This month</option>
|
||||
<option selected={value === "next_month" } value="next_month">Next month</option>
|
||||
<option selected={value === "last_month" } value="last_month">Last month</option>
|
||||
<option selected={value === "all_time" } value="all_time">All time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="par">)</div>
|
||||
<div class="par">)
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { significantDecimals } from "$lib/utils/numbers";
|
||||
|
||||
export let target = 1;
|
||||
export let count = 0;
|
||||
export let green = false;
|
||||
export let alwaysSmooth = false;
|
||||
export let percentageOnly = false;
|
||||
export let weighted = false;
|
||||
export let compact = false;
|
||||
export let planned: number = null;
|
||||
|
||||
export let fullwidth = false;
|
||||
export let name: string;
|
||||
@@ -24,9 +28,12 @@
|
||||
|
||||
boat = ($now.getTime() - from.getTime()) / (to.getTime() - from.getTime());
|
||||
boatEnabled = (boat > 0 && boat < 1)
|
||||
} else if (planned != null) {
|
||||
boat = Math.min(1, Math.max(planned, count) / target);
|
||||
boatEnabled = true;
|
||||
} else {
|
||||
boatEnabled = false;
|
||||
boat = 0;
|
||||
boatEnabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -38,7 +45,11 @@
|
||||
{#if percentageOnly}
|
||||
<span class="ackreq">({((count / target) * 100).toFixed(0)}%)</span>
|
||||
{:else}
|
||||
<span class="ackreq">({Math.floor(count)} / {Math.ceil(target)})</span>
|
||||
{#if weighted}
|
||||
<span class="ackreq">({significantDecimals(count)} / {significantDecimals(target)})</span>
|
||||
{:else}
|
||||
<span class="ackreq">({Math.round(count)} / {Math.round(target)})</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -10,10 +10,19 @@
|
||||
|
||||
<div class="markdown">{@html outputHtml}</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
@import "../../css/colors.sass";
|
||||
|
||||
div.markdown :global(p) {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
margin: 0.5em 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.markdown :global(code) {
|
||||
@@ -58,6 +67,17 @@
|
||||
counter-reset: li;
|
||||
}
|
||||
|
||||
div.markdown :global(a) {
|
||||
color: #d1a31b;
|
||||
text-decoration: none;
|
||||
transition: 250ms;
|
||||
|
||||
&:hover {
|
||||
color: #FFCC11;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
div.markdown :global(ul) :global(li:before) {
|
||||
content: "\2022"; /* Add content: \2022 is the CSS Code/unicode for a bullet */
|
||||
color: #333; /* Change the color */
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
@media screen and (max-width: 749px) {
|
||||
> div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -9,18 +9,19 @@
|
||||
export let error: string | null = null;
|
||||
export let closable: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let nobody: boolean = false;
|
||||
|
||||
const {closeModal} = getModalContext();
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="modal-background">
|
||||
<div class="modal" class:wide>
|
||||
<div class="header">
|
||||
<div class="modal" class:wide class:nobody>
|
||||
<div class="header" class:nobody>
|
||||
<div class="title" class:noclose={!closable}>{verb} {noun}</div>
|
||||
{#if (closable)}
|
||||
<div class="x">
|
||||
<div class="button" on:click={closeModal}>
|
||||
<div class="button" on:click={closeModal} class:nobody>
|
||||
<Icon name="times" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,10 +30,10 @@
|
||||
{#if (error != null)}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
<div class="body">
|
||||
<div class="body" class:nobody>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<div class="button-row" class:nobody>
|
||||
<button disabled={disabled} type="submit">{verb} {noun}</button>
|
||||
<button disabled={!closable} on:click|preventDefault={closeModal} >Cancel</button>
|
||||
</div>
|
||||
@@ -64,6 +65,10 @@
|
||||
border-radius: 0.5em;
|
||||
|
||||
background: $color-entry1;
|
||||
|
||||
&.nobody {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
div.modal.wide {
|
||||
max-width: 80ch;
|
||||
@@ -71,7 +76,6 @@
|
||||
|
||||
div.header {
|
||||
padding: 1em;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -94,6 +98,10 @@
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media screen and (max-width: 749px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
div.title {
|
||||
@@ -145,6 +153,10 @@
|
||||
button:disabled {
|
||||
color: $color-entry4;
|
||||
}
|
||||
|
||||
&.nobody {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
div.modal :global(label) {
|
||||
@@ -172,7 +184,7 @@
|
||||
padding: 0.45em 1ch;
|
||||
width: 100%;
|
||||
}
|
||||
div.modal :global(select option) {
|
||||
div.modal :global(select option), div.modal :global(select optgroup) {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
<style lang="sass">
|
||||
div.modal-body
|
||||
padding: 1em
|
||||
padding: 0 1em
|
||||
padding-left: 0
|
||||
flex: 1
|
||||
|
||||
div.modal-body:first-of-type
|
||||
padding-left: 1em
|
||||
@media screen and (max-width: 749px)
|
||||
padding-left: 1em
|
||||
padding: 0 1em
|
||||
|
||||
&:first-of-type
|
||||
padding-left: 1em
|
||||
</style>
|
||||
@@ -67,12 +67,30 @@
|
||||
// Mark it non-segmented if the target is too high. This prevents a clunky progress bar,
|
||||
// or a browser freeze if you set the target very high.
|
||||
nonSegmented = alwaysSmooth || (target >= 60);
|
||||
|
||||
if (!nonSegmented) {
|
||||
ons = Math.floor(ons);
|
||||
offs = Math.ceil(offs);
|
||||
if (ons + offs > target) {
|
||||
if (offs > 0) {
|
||||
offs -= 1;
|
||||
} else {
|
||||
ons -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
offClass = "none";
|
||||
nonSegmented = alwaysSmooth;
|
||||
ons = 0;
|
||||
offs = 1;
|
||||
}
|
||||
|
||||
if (nonSegmented && (ons + offs) < 1) {
|
||||
const factor = (ons + offs);
|
||||
ons = (ons / factor)
|
||||
offs = (offs / factor)
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
3: "spinner",
|
||||
4: "check",
|
||||
5: "times",
|
||||
6: "times",
|
||||
6: "trash",
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
export let names: string[]
|
||||
</script>
|
||||
|
||||
<div class="tag-row">
|
||||
{#each names as name (name)}
|
||||
<div class="tag">{name}<span class="comma">,</span></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
div.tag-row
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
> div.tag
|
||||
margin: 0.25em 0.5ch
|
||||
background-color: $color-entry2
|
||||
padding: 0.25em 1ch
|
||||
border-radius: 0.25em
|
||||
font-size: 0.75em
|
||||
|
||||
> span.comma
|
||||
font-size: 0
|
||||
|
||||
&:first-child
|
||||
margin-left: 0
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an SVG x-axis. This component is also configured to detect if your x-scale is an ordinal scale. If so, it will place the markers in the middle of the bandwidth.
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
const { width, height, xScale, yRange } = getContext('LayerCake');
|
||||
|
||||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space */
|
||||
export let gridlines = true;
|
||||
|
||||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */
|
||||
export let tickMarks = false;
|
||||
|
||||
/** @type {Boolean} [baseline=false] – Show a solid line at the bottom. */
|
||||
export let baseline = false;
|
||||
|
||||
/** @type {Boolean} [snapTicks=false] - Instead of centering the text on the first and the last items, align them to the edges of the chart. */
|
||||
export let snapTicks = false;
|
||||
|
||||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
|
||||
export let formatTick = d => d;
|
||||
|
||||
/** @type {Number|Array|Function} [ticks] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. If nothing, it uses the default ticks supplied by the D3 function. */
|
||||
export let ticks = undefined;
|
||||
|
||||
/** @type {Number} [xTick=0] - TK */
|
||||
export let xTick = 0;
|
||||
|
||||
/** @type {Number} [yTick=16] - The distance from the baseline to place each tick value. */
|
||||
export let yTick = 16;
|
||||
|
||||
$: isBandwidth = typeof $xScale.bandwidth === 'function';
|
||||
|
||||
$: tickVals = Array.isArray(ticks) ? ticks :
|
||||
isBandwidth ?
|
||||
$xScale.domain() :
|
||||
typeof ticks === 'function' ?
|
||||
ticks($xScale.ticks()) :
|
||||
$xScale.ticks(ticks);
|
||||
|
||||
function textAnchor(i) {
|
||||
if (snapTicks === true) {
|
||||
if (i === 0) {
|
||||
return 'start';
|
||||
}
|
||||
if (i === tickVals.length - 1) {
|
||||
return 'end';
|
||||
}
|
||||
}
|
||||
return 'middle';
|
||||
}
|
||||
</script>
|
||||
|
||||
<g class="axis x-axis" class:snapTicks>
|
||||
{#each tickVals as tick, i (tick)}
|
||||
<g class="tick tick-{i}" transform="translate({$xScale(tick)},{Math.max(...$yRange)})">
|
||||
{#if gridlines !== false}
|
||||
<line class="gridline" y1={$height * -1} y2="0" x1="0" x2="0" />
|
||||
{/if}
|
||||
{#if tickMarks === true}
|
||||
<line
|
||||
class="tick-mark"
|
||||
y1={0}
|
||||
y2={6}
|
||||
x1={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0}
|
||||
x2={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0}
|
||||
/>
|
||||
{/if}
|
||||
<text
|
||||
x={xTick || isBandwidth ? $xScale.bandwidth() / 2 : 0}
|
||||
y={8}
|
||||
dx=""
|
||||
dy=""
|
||||
font-size="0.66em"
|
||||
text-anchor={textAnchor(i)}>{formatTick(tick)}</text
|
||||
>
|
||||
</g>
|
||||
{/each}
|
||||
{#if baseline === true}
|
||||
<line class="baseline" y1={$height + 0.5} y2={$height + 0.5} x1="0" x2={$width} />
|
||||
{/if}
|
||||
</g>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../../css/colors.sass";
|
||||
|
||||
.tick {
|
||||
font-size: 0.725em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
line,
|
||||
.tick line {
|
||||
stroke: $color-entry2;
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: $color-entry5;
|
||||
}
|
||||
|
||||
.tick .tick-mark,
|
||||
.baseline {
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
/* This looks slightly better */
|
||||
.axis.snapTicks .tick:last-child text {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
.axis.snapTicks .tick.tick-0 text {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an HTML y-axis.
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { padding, xRange, yScale } = getContext('LayerCake');
|
||||
|
||||
/** @type {Boolean} [gridlines=true] - Extend lines from the ticks into the chart space */
|
||||
export let gridlines = true;
|
||||
|
||||
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */
|
||||
export let tickMarks = false;
|
||||
|
||||
/** @type {Function} [formatTick=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
|
||||
export let formatTick = d => d;
|
||||
|
||||
/** @type {Number|Array|Function} [ticks=4] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. */
|
||||
export let ticks = 4;
|
||||
|
||||
/** @type {Number} [xTick=0] - How far over to position the text marker. */
|
||||
export let xTick = 0;
|
||||
|
||||
/** @type {Number} [yTick=0] - How far up and down to position the text marker. */
|
||||
export let yTick = 0;
|
||||
|
||||
/** @type {Number} [dxTick=0] - Any optional value passed to the `dx` attribute on the text marker and tick mark (if visible). This is ignored on the text marker if your scale is ordinal. */
|
||||
export let dxTick = 0;
|
||||
|
||||
/** @type {Number} [dyTick=-4] - Any optional value passed to the `dy` attribute on the text marker and tick mark (if visible). This is ignored on the text marker if your scale is ordinal. */
|
||||
export let dyTick = -4;
|
||||
|
||||
/** @type {String} [textAnchor='start'] The CSS `text-anchor` passed to the label. This is automatically set to "end" if the scale has a bandwidth method, like in ordinal scales. */
|
||||
export let textAnchor = 'start';
|
||||
|
||||
$: isBandwidth = typeof $yScale.bandwidth === 'function';
|
||||
|
||||
$: tickVals = Array.isArray(ticks) ? ticks :
|
||||
isBandwidth ?
|
||||
$yScale.domain() :
|
||||
typeof ticks === 'function' ?
|
||||
ticks($yScale.ticks()) :
|
||||
$yScale.ticks(ticks);
|
||||
</script>
|
||||
|
||||
<g class='axis y-axis' transform='translate({-$padding.left}, 0)'>
|
||||
{#each tickVals as tick (tick)}
|
||||
<g class='tick tick-{tick}' transform='translate({$xRange[0] + (isBandwidth ? $padding.left : 0)}, {$yScale(tick)})'>
|
||||
{#if gridlines !== false}
|
||||
<line
|
||||
class="gridline"
|
||||
x1={5}
|
||||
x2='98.%'
|
||||
y1={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)}
|
||||
y2={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)}
|
||||
></line>
|
||||
{/if}
|
||||
{#if tickMarks === true}
|
||||
<line
|
||||
class='tick-mark'
|
||||
x1='0'
|
||||
x2='{isBandwidth ? -6 : 6}'
|
||||
y1={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)}
|
||||
y2={yTick + (isBandwidth ? ($yScale.bandwidth() / 2) : 0)}
|
||||
></line>
|
||||
{/if}
|
||||
<text
|
||||
x='{xTick}'
|
||||
y='{yTick + (isBandwidth ? $yScale.bandwidth() / 2 : 0)}'
|
||||
dx='{isBandwidth ? -9 : dxTick}'
|
||||
dy='{isBandwidth ? 4 : dyTick}'
|
||||
style="text-anchor:{isBandwidth ? 'end' : textAnchor};"
|
||||
>{formatTick(tick)}</text>
|
||||
</g>
|
||||
{/each}
|
||||
</g>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../../css/colors.sass";
|
||||
|
||||
.tick {
|
||||
font-size: .725em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.tick line {
|
||||
stroke: $color-entry2;
|
||||
}
|
||||
.tick .gridline {
|
||||
stroke-dasharray: 2;
|
||||
}
|
||||
|
||||
.tick text {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.tick.tick-0 line {
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
@component
|
||||
Generates an SVG area shape using the `area` function from [d3-shape](https://github.com/d3/d3-shape).
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { data, xGet, yGet } = getContext('LayerCake');
|
||||
|
||||
/** @type {String} [stroke='#ab00d6'] - The shape's fill color. This is technically optional because it comes with a default value but you'll likely want to replace it with your own color. */
|
||||
export let stroke = '#ab00d6';
|
||||
|
||||
$: path = 'M' + $data
|
||||
.map(d => $xGet(d) + ',' + $yGet(d))
|
||||
.join('L');
|
||||
</script>
|
||||
|
||||
<path class='path-line' d='{path}' {stroke}></path>
|
||||
|
||||
<style>
|
||||
.path-line {
|
||||
fill: none;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -25,13 +25,14 @@
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter, ItemGroup } from "$lib/models/item";
|
||||
import { groupItems } from "$lib/utils/items";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
export let items: Item[];
|
||||
export let groups: ItemGroup[];
|
||||
export let filter: ItemFilter;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let itemsWritable = writable<Item[]>(items);
|
||||
let groupsWritable = writable<ItemGroup[]>(groups);
|
||||
@@ -50,9 +51,9 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const newItems = await sl3(fetch).listItems($scope.id, filter)
|
||||
itemsWritable.set(newItems);
|
||||
groupsWritable.set(groupItems(newItems, filter.scheduledDate.min))
|
||||
const res = await sl3(fetch).listItems($scope.id, filter, true, {});
|
||||
itemsWritable.set(res.items);
|
||||
groupsWritable.set(res.groups);
|
||||
} catch(_) {}
|
||||
|
||||
loading = false;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" context="module">
|
||||
const contextKey = {ctx: "itemMultiListContext"};
|
||||
|
||||
interface ItemMultiListContextData {
|
||||
lists: Readable<Record<string, Item[]>>,
|
||||
reloadItemLists(): void,
|
||||
};
|
||||
|
||||
const fallback: ItemMultiListContextData = {
|
||||
lists: readable({}),
|
||||
reloadItemLists() {},
|
||||
}
|
||||
|
||||
export function getItemMultiListContext() {
|
||||
return getContext(contextKey) as ItemMultiListContextData || fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import deepEqual from "$lib/utils/object";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {page} = getStores();
|
||||
|
||||
export let lists: Record<string, Item[]>
|
||||
export let filters: Record<string, ItemFilter>
|
||||
export let global: boolean = false;
|
||||
|
||||
let loaded: Record<string, ItemFilter> = {...filters};
|
||||
let loading: Record<string, boolean> = {};
|
||||
const listsWritable = writable<Record<string, Item[]>>({...lists});
|
||||
|
||||
function reloadItemLists() {
|
||||
loaded = {};
|
||||
}
|
||||
|
||||
async function runLoad(key: string, filter: ItemFilter) {
|
||||
loading = {...loading, [key]: true};
|
||||
|
||||
try {
|
||||
const items = await sl3(fetch).listItems(global ? "ALL" : $scope.id, filter)
|
||||
listsWritable.update(l => ({...l, [key.replace("Filter", "Items")]: items}));
|
||||
} catch(_err) {
|
||||
// TODO: Use err
|
||||
}
|
||||
|
||||
loading = {...loading, [key]: false};
|
||||
loaded = {...loaded, [key]: filter};
|
||||
}
|
||||
|
||||
setContext<ItemMultiListContextData>(contextKey, {
|
||||
lists: {subscribe: listsWritable.subscribe},
|
||||
reloadItemLists,
|
||||
});
|
||||
|
||||
$: for (const key in filters) {
|
||||
if (loading[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loaded[key] == null || !deepEqual(filters[key], loaded[key])) {
|
||||
runLoad(key, {...filters[key]})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
@@ -3,16 +3,16 @@
|
||||
|
||||
export type ModalData =
|
||||
| { name: "closed" }
|
||||
| { name: "scope.create" }
|
||||
| { name: "scope.edit", scope: Scope }
|
||||
| { name: "scope.delete", scope: Scope }
|
||||
| { name: "item.create", requirement?: Requirement }
|
||||
| { name: "scope.create" }
|
||||
| { name: "scope.edit", scope: Scope }
|
||||
| { name: "scope.delete", scope: Scope }
|
||||
| { name: "item.create", requirement?: RequirementWithoutItems }
|
||||
| { name: "item.edit", item: Item }
|
||||
| { name: "item.acquire", item: Item }
|
||||
| { name: "item.delete", item: Item }
|
||||
| { name: "requirement.create", project: ProjectEntry }
|
||||
| { name: "requirement.edit", requirement: Requirement }
|
||||
| { name: "requirement.delete", requirement: Requirement }
|
||||
| { name: "requirement.edit", requirement: RequirementWithoutItems }
|
||||
| { name: "requirement.delete", requirement: RequirementWithoutItems }
|
||||
| { name: "project.create" }
|
||||
| { name: "project.edit", project: Project }
|
||||
| { name: "project.delete", project: Project }
|
||||
@@ -22,6 +22,8 @@
|
||||
| { name: "sprint.create" }
|
||||
| { name: "sprint.edit", sprint: Sprint }
|
||||
| { name: "sprint.delete", sprint: Sprint }
|
||||
| { name: "requirement.jump" }
|
||||
| { name: "" }
|
||||
|
||||
interface ModalContextData {
|
||||
currentModal: Readable<ModalData>
|
||||
@@ -37,12 +39,12 @@
|
||||
<script lang="ts">
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
import { getContext, onMount, setContext } from "svelte";
|
||||
import type { ProjectEntry, Requirement } from "$lib/models/project";
|
||||
import type { ProjectEntry, Requirement, RequirementWithoutItems } from "$lib/models/project";
|
||||
import type Item from "$lib/models/item";
|
||||
import type Project from "$lib/models/project";
|
||||
import type Stat from "$lib/models/stat";
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import type Scope from "$lib/models/scope";
|
||||
|
||||
let store = writable<ModalData>({name: "closed"});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
};
|
||||
|
||||
const fallback: ProjectContextData = {
|
||||
project: writable({} as Project),
|
||||
project: readable({requirements: []} as Project),
|
||||
reloadProject: () => Promise.resolve()
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import type Project from "$lib/models/project";
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
@@ -44,12 +44,15 @@
|
||||
try {
|
||||
const newProject = await sl3(fetch).findProject($scope.id, project.id)
|
||||
projectWritable.set(newProject);
|
||||
} catch(_) {}
|
||||
} catch(e) {
|
||||
console.warn("Failed to reload project:", e);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
$: if (project !== lastSet) {
|
||||
lastSet = project;
|
||||
projectWritable.set(project);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,10 +22,12 @@
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import type { ProjectEntry } from "$lib/models/project";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
export let projects: ProjectEntry[];
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let projectWritable = writable<ProjectEntry[]>(projects);
|
||||
let loading = false;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" context="module">
|
||||
const contextKey = {ctx: "requirementListCtx"};
|
||||
|
||||
interface RequirementListContextData {
|
||||
requirements: Readable<RequirementWithoutItems[]>,
|
||||
reloadRequirementList(): Promise<void>,
|
||||
};
|
||||
|
||||
const fallback: RequirementListContextData = {
|
||||
requirements: readable([]),
|
||||
reloadRequirementList: () => Promise.resolve()
|
||||
};
|
||||
|
||||
export function getRequirementListContext() {
|
||||
return getContext(contextKey) as RequirementListContextData || fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, onMount, setContext } from "svelte";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import type { RequirementWithoutItems } from "$lib/models/project";
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
|
||||
let requirementsWritable = writable<RequirementWithoutItems[]>([]);
|
||||
let loading = false;
|
||||
|
||||
setContext<RequirementListContextData>(contextKey, {
|
||||
requirements: {subscribe: requirementsWritable.subscribe},
|
||||
reloadRequirementList,
|
||||
});
|
||||
|
||||
async function reloadRequirementList() {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const newProjects = await sl3(fetch).listRequirements($scope?.id || null)
|
||||
requirementsWritable.set(newProjects);
|
||||
} catch(_) {}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
@@ -10,8 +10,8 @@
|
||||
};
|
||||
|
||||
const fallback: ScopeContextData = {
|
||||
scope: readable({} as Scope),
|
||||
lastHistoryRange: writable(""),
|
||||
scope: readable({stats: []} as Scope),
|
||||
lastHistoryRange: writable("last:14d"),
|
||||
upsertStat: () => {},
|
||||
deleteStat: () => {},
|
||||
updateScope: () => {},
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
import { readable, writable, type Readable } from "svelte/store";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "./ScopeContext.svelte";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
export let scopes: Scope[];
|
||||
|
||||
const {page} = getStores();
|
||||
|
||||
let scopesWritable = writable<Scope[]>(scopes);
|
||||
let loading = false;
|
||||
let lastSet = scopes;
|
||||
|
||||
@@ -24,12 +24,15 @@
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
import parseInterval from "$lib/utils/timeinterval";
|
||||
import { getTimeContext } from "./TimeContext.svelte";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
export let sprints: Sprint[];
|
||||
export let intervalString: string;
|
||||
export let global: boolean = false;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {now} = getTimeContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let sprintsWritable = writable<Sprint[]>(sprints);
|
||||
let loading = false;
|
||||
@@ -46,7 +49,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const newSprints = await sl3(fetch).listSprints($scope.id, parseInterval(intervalString, $now));
|
||||
const newSprints = await sl3(fetch).listSprints(global ? "ALL" : $scope.id, parseInterval(intervalString, $now));
|
||||
sprintsWritable.set(newSprints);
|
||||
} catch(_) {}
|
||||
|
||||
|
||||
@@ -1,69 +1,146 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface PartOptionGroup {
|
||||
id: number
|
||||
label: string
|
||||
subs: PartOptionGroup[]
|
||||
options: PartOption[]
|
||||
}
|
||||
|
||||
export interface PartOption {
|
||||
partId: number
|
||||
name: string
|
||||
enabled: boolean
|
||||
required: number
|
||||
}
|
||||
|
||||
const SO = [
|
||||
Status.Active, Status.Background, Status.Available, Status.Blocked,
|
||||
Status.Completed, Status.Failed, Status.Dropped,
|
||||
]
|
||||
|
||||
function compareStatus<T extends {status: Status, id: number}>(a: T, b: T): number {
|
||||
if (a.status === b.status) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
|
||||
return SO.indexOf(a.status) - SO.indexOf(b.status)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/env";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import { SprintKind, type SprintInputPart } from "$lib/models/sprint";
|
||||
import Status from "$lib/models/status";
|
||||
import shortestDecimal from "$lib/utils/numbers";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
import Checkbox from "../layout/Checkbox.svelte";
|
||||
|
||||
export let value: SprintInputPart[] = [];
|
||||
export let kind: SprintKind;
|
||||
export let scopeId: number;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
let error = "";
|
||||
let options: PartOption[] = [];
|
||||
let groups: PartOptionGroup[] = [];
|
||||
let showRequired = false;
|
||||
let loading = false;
|
||||
let onlyActive = true;
|
||||
let loaded: SprintKind = SprintKind.Invalid
|
||||
let hiddenSelections: SprintInputPart[] = [];
|
||||
let actualScope: Scope;
|
||||
|
||||
async function load(kind: SprintKind) {
|
||||
function forceReload() {
|
||||
loaded = SprintKind.Invalid;
|
||||
}
|
||||
|
||||
async function load(kind: SprintKind, scope: Scope) {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
switch (kind) {
|
||||
case SprintKind.Items: {
|
||||
const items = await sl3(fetch).listItems($scope.id, {});
|
||||
options = items.map(i => ({
|
||||
enabled: false,
|
||||
name: `${i.name}${i.acquiredTime?" ✓":""}`,
|
||||
partId: i.id,
|
||||
required: 0,
|
||||
const items = await sl3(fetch).listItems(scope.id, {unAcquired: onlyActive});
|
||||
const projects = items.filter(i => i.project != null).map(i => i.project).sort(compareStatus).filter((p, i, a) => p.id != a[i-1]?.id);
|
||||
const requirements = items.filter(i => i.requirement != null).map(i => i.requirement).sort(compareStatus).filter((r, i, a) => r.id != a[i-1]?.id);
|
||||
const loose = items.filter(i => i.requirementId == null);
|
||||
const reqProjectIdMap = items.filter(i => i.projectId != null && i.requirementId != null).reduce((p, c) => ({
|
||||
...p,
|
||||
[c.requirementId]: c.projectId,
|
||||
}), {} as Record<number, string>)
|
||||
|
||||
groups = projects.filter(p => !onlyActive || p.status === Status.Active || p.status === Status.Background).map(p => ({
|
||||
id: p.id,
|
||||
label: `${p.name} (${p.statusName})`,
|
||||
options: [],
|
||||
subs: requirements.filter(r => !onlyActive || r.status === Status.Active || r.status === Status.Background).filter(r => reqProjectIdMap[r.id] === p.id).map(r => ({
|
||||
id: r.id,
|
||||
label: `${r.name} (${r.statusName})`,
|
||||
options: items.filter(i => i.requirementId === r.id).map(i => ({
|
||||
enabled: false,
|
||||
partId: i.id,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
|
||||
required: 0,
|
||||
})).sort((a,b) => a.partId - b.partId),
|
||||
subs: [],
|
||||
}))
|
||||
}))
|
||||
|
||||
groups.push({id: -1, label: "Loose items", subs: [], options: loose.map(i => ({
|
||||
enabled: false,
|
||||
partId: i.id,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
|
||||
required: 0,
|
||||
})).sort((a,b) => a.partId - b.partId)})
|
||||
|
||||
const allParts = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]);
|
||||
hiddenSelections = value.filter(v => !allParts.find(p => p.partId === v.partId));
|
||||
|
||||
showRequired = false;
|
||||
break;
|
||||
}
|
||||
case SprintKind.Requirements: {
|
||||
options = [];
|
||||
groups = [];
|
||||
throw "Requirement selection is not yet supported the UI."
|
||||
}
|
||||
case SprintKind.Stats: {
|
||||
options = $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0}));
|
||||
groups = [{
|
||||
id: -1,
|
||||
label: "",
|
||||
subs: [],
|
||||
options: scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
|
||||
}];
|
||||
showRequired = true;
|
||||
break;
|
||||
}
|
||||
case SprintKind.Scope: {
|
||||
options = [];
|
||||
groups = [];
|
||||
showRequired = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
error = "";
|
||||
for (const opt of options) {
|
||||
const val = value.find(v => v.partId === opt.partId);
|
||||
if (val != null) {
|
||||
opt.enabled = true;
|
||||
opt.required = val.required;
|
||||
for (const group of groups) {
|
||||
for (const opt of group.options) {
|
||||
const val = value.find(v => v.partId === opt.partId);
|
||||
if (val != null) {
|
||||
opt.enabled = true;
|
||||
opt.required = val.required;
|
||||
}
|
||||
}
|
||||
|
||||
for (const subGroup of group.subs) {
|
||||
for (const opt of subGroup.options) {
|
||||
const val = value.find(v => v.partId === opt.partId);
|
||||
if (val != null) {
|
||||
opt.enabled = true;
|
||||
opt.required = val.required;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
@@ -74,26 +151,55 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
$: if (loaded === kind && error === "") {
|
||||
value = options.filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
|
||||
// Hax to make it reload if onlyActive changed.
|
||||
$: if (onlyActive != null) {
|
||||
forceReload();
|
||||
}
|
||||
|
||||
$: if (loaded === kind && error === "") {
|
||||
value = [
|
||||
...hiddenSelections,
|
||||
...groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required})),
|
||||
]
|
||||
}
|
||||
|
||||
$: actualScope = ($scope.id === scopeId || scopeId == null) ? $scope : $scopes.find(s => s.id === scopeId);
|
||||
|
||||
$: if (browser && !loading && loaded !== kind) {
|
||||
load(kind);
|
||||
load(kind, actualScope);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="part-input">
|
||||
<div class="error">{error}</div>
|
||||
{#each options as option (option.partId)}
|
||||
<div class="part">
|
||||
<Checkbox noLabel bind:checked={option.enabled} />
|
||||
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
|
||||
{#if showRequired && option.enabled}
|
||||
<input type="number" class="required" bind:value={option.required} />
|
||||
{/if}
|
||||
</div>
|
||||
{#each groups as group (group.id)}
|
||||
<div class="label">{group.label}</div>
|
||||
{#each group.subs as group (group.id)}
|
||||
<div class="label label-2">{group.label}</div>
|
||||
{#each group.options as option (option.partId)}
|
||||
<div class="part">
|
||||
<Checkbox noLabel bind:checked={option.enabled} />
|
||||
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
|
||||
{#if showRequired && option.enabled}
|
||||
<input type="number" class="required" bind:value={option.required} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{#each group.options as option (option.partId)}
|
||||
<div class="part">
|
||||
<Checkbox noLabel bind:checked={option.enabled} />
|
||||
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
|
||||
{#if showRequired && option.enabled}
|
||||
<input type="number" class="required" bind:value={option.required} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{#if kind === SprintKind.Items}
|
||||
<hr />
|
||||
<Checkbox label="Show only active items" bind:checked={onlyActive} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -103,7 +209,6 @@
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 1px solid $color-entry1;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
@@ -123,7 +228,7 @@
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0.3em 0.5ch;
|
||||
padding: 0.25em 0.5ch;
|
||||
|
||||
&.disabled {
|
||||
color: $color-entry2;
|
||||
@@ -131,11 +236,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 2em;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $color-entry2;
|
||||
}
|
||||
|
||||
div.label {
|
||||
margin-top: 0.5em;
|
||||
border-bottom: 1px solid $color-entry2;
|
||||
|
||||
&:empty {
|
||||
margin-top: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.label-2 {
|
||||
margin-top: 1em;
|
||||
font-size: 0.75em;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&+div.label-2 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
div.part-input {
|
||||
width: calc(100% - 2ch);
|
||||
margin-bottom: 1em;
|
||||
margin-top: 0.20em;
|
||||
height: 12em;
|
||||
height: 26em;
|
||||
overflow-y: auto;
|
||||
|
||||
background: $color-entryhalf;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type { ProjectEntry } from "$lib/models/project";
|
||||
import { STATUS_NAMES } from "$lib/models/status";
|
||||
import type Status from "$lib/models/status";
|
||||
import { getProjectListContext } from "../contexts/ProjectListContext.svelte";
|
||||
import { STATUS_ORDER } from "../scope/ProjectMenu.svelte";
|
||||
|
||||
const { projects } = getProjectListContext();
|
||||
|
||||
export let value: number;
|
||||
export let disabled: boolean = false;
|
||||
|
||||
let projectsByStatus: Record<Status, ProjectEntry[]>;
|
||||
|
||||
$: projectsByStatus = $projects.reduce((p, c) => (
|
||||
{...p, [c.status]: [...p[c.status], c]}
|
||||
), {0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: []});
|
||||
</script>
|
||||
|
||||
<select disabled={disabled} bind:value={value}>
|
||||
{#each STATUS_ORDER as status (status)}
|
||||
{#if projectsByStatus[status].length > 0}
|
||||
<optgroup label={STATUS_NAMES[status]}>
|
||||
{#each projectsByStatus[status] as project (project.id)}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { Requirement } from "$lib/models/project";
|
||||
|
||||
export let value: number;
|
||||
export let requirements: Requirement[];
|
||||
export let disabled: boolean = false;
|
||||
</script>
|
||||
|
||||
<select disabled={disabled} bind:value={value}>
|
||||
{#each requirements as req (req.id)}
|
||||
<option value={req.id}>{req.name} ({req.statusName})</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { endOfMonth, endOfWeek, endOfYear, formatDate, nextMonth, nextYear } from "$lib/utils/date";
|
||||
|
||||
export let value: string;
|
||||
export let openDate: Date;
|
||||
export let notScheduledText = "Not Scheduled";
|
||||
|
||||
let selected: "none" | "date" | "eod" | "eot" | "eow" | "eonw" | "eonnw" | "eom" | "eonm" | "eoy" | "eony" = !!value ? "date" : "none";
|
||||
|
||||
$: {
|
||||
switch (selected) {
|
||||
case "eod":
|
||||
value = formatDate(openDate);
|
||||
break;
|
||||
case "eot":
|
||||
value = formatDate(new Date(openDate.getTime() + 86400000));
|
||||
break;
|
||||
case "eow":
|
||||
value = formatDate(endOfWeek(openDate))
|
||||
break;
|
||||
case "eonw":
|
||||
value = formatDate(endOfWeek(new Date(openDate.getTime() + (86400000 * 7))))
|
||||
break;
|
||||
case "eonnw":
|
||||
value = formatDate(endOfWeek(new Date(openDate.getTime() + (86400000 * 14))))
|
||||
break;
|
||||
case "eom":
|
||||
value = formatDate(endOfMonth(openDate))
|
||||
break;
|
||||
case "eonm":
|
||||
value = formatDate(endOfMonth(nextMonth(openDate)))
|
||||
break;
|
||||
case "eoy":
|
||||
value = formatDate(endOfYear(openDate))
|
||||
break;
|
||||
case "eony":
|
||||
value = formatDate(endOfYear(nextYear(openDate)))
|
||||
break;
|
||||
case "date":
|
||||
if (value == "") {
|
||||
value = formatDate(openDate);
|
||||
}
|
||||
break;
|
||||
case "none":
|
||||
value = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<select bind:value={selected}>
|
||||
<option value="none">{notScheduledText}</option>
|
||||
<option value="eod">Today</option>
|
||||
<option value="eot">Tomorrow</option>
|
||||
<option value="eow">Sunday</option>
|
||||
<option value="eonw">Next Sunday</option>
|
||||
<option value="eonnw">The Sunday after Next</option>
|
||||
<option value="eom">End of Month</option>
|
||||
<option value="eonm">End of Next Month</option>
|
||||
<option value="eoy">End of Year</option>
|
||||
<option value="eony">End of Next Year</option>
|
||||
<option value="date">Specific Date</option>
|
||||
</select>
|
||||
|
||||
{#if selected !== "none"}
|
||||
<input type="date" disabled={selected !== "date"} bind:value={value} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
select {
|
||||
margin-bottom: 0.25em !important;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-top: 0 !important;
|
||||
resize: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
import type { StatValueInput } from "$lib/models/stat";
|
||||
import Checkbox from "../layout/Checkbox.svelte";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
|
||||
export let value: StatValueInput[] = [];
|
||||
export let showAcquired = false;
|
||||
@@ -9,50 +11,61 @@
|
||||
export let hideUnseen = false;
|
||||
export let zeroDeletes = false;
|
||||
export let noToggle = false;
|
||||
export let initOnList: number[] = [];
|
||||
export let overrideScopeId: number = null;
|
||||
|
||||
const {scopes} = getScopeListContext();
|
||||
const {scope} = getScopeContext();
|
||||
|
||||
let acquiredMap: Record<number, number>;
|
||||
let requiredMap: Record<number, number>;
|
||||
let enabledMap: Record<number, boolean>;
|
||||
let actualScope: Scope;
|
||||
let value2: any;
|
||||
|
||||
function initialize() {
|
||||
function initialize(actualScope: Scope) {
|
||||
acquiredMap = {};
|
||||
requiredMap = {};
|
||||
enabledMap = {};
|
||||
|
||||
if (hideUnseen) {
|
||||
for (const v of value) {
|
||||
enabledMap[v.statId] = zeroDeletes ? v.required > 0 : v.required >= 0;
|
||||
enabledMap[v.statId] = initOnList.includes(v.statId) || (zeroDeletes ? v.required > 0 : v.required >= 0);
|
||||
requiredMap[v.statId] = v.required;
|
||||
acquiredMap[v.statId] = v.acquired;
|
||||
}
|
||||
} else {
|
||||
for (const stat of $scope.stats) {
|
||||
for (const stat of actualScope.stats) {
|
||||
const found = value.find(v => v.statId == stat.id);
|
||||
|
||||
enabledMap[stat.id] = found != null;
|
||||
enabledMap[stat.id] = found != null && (found?.required > (zeroDeletes ? 0 : -1));
|
||||
requiredMap[stat.id] = found?.required || 0;
|
||||
acquiredMap[stat.id] = found?.acquired || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: actualScope = $scopes.find(s => s.id === overrideScopeId) || $scope;
|
||||
|
||||
$: {
|
||||
if (acquiredMap == null) {
|
||||
initialize();
|
||||
initialize(actualScope);
|
||||
}
|
||||
}
|
||||
|
||||
$: value = $scope.stats.filter(s => enabledMap[s.id]).map(s => ({
|
||||
statId: s.id,
|
||||
required: requiredMap[s.id],
|
||||
acquired: acquiredMap[s.id],
|
||||
}));
|
||||
$: {
|
||||
value = actualScope.stats.filter(s => enabledMap[s.id]).map(s => ({
|
||||
statId: s.id,
|
||||
required: requiredMap[s.id],
|
||||
acquired: acquiredMap[s.id],
|
||||
}));
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
value2 = value; // hax
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="stat-input">
|
||||
{#each $scope.stats as stat (stat.id)}
|
||||
{#each actualScope.stats as stat (stat.id)}
|
||||
{#if enabledMap[stat.id] != null}
|
||||
<div class="stat">
|
||||
{#if !noToggle}
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
<script lang="ts">
|
||||
import Status from "$lib/models/status";
|
||||
<script lang="ts" context="module">
|
||||
const STATUS_ENTRIES: [Status, IconName][] = Object.entries(STATUS_ICONS).map(e => [parseInt(e[0]) as Status, e[1]])
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type Status from "$lib/models/status";
|
||||
import { STATUS_NAMES } from "$lib/models/status";
|
||||
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
|
||||
import Icon, { type IconName } from "../layout/Icon.svelte";
|
||||
|
||||
export let status: Status;
|
||||
</script>
|
||||
|
||||
<select class={`sc-${status}`} bind:value={status}>
|
||||
<option class="sc-0" value={Status.Blocked}>Blocked</option>
|
||||
<option class="sc-1" value={Status.Available}>Available</option>
|
||||
<option class="sc-2" value={Status.Background}>Background</option>
|
||||
<option class="sc-3" value={Status.Active}>Active</option>
|
||||
<option class="sc-4" value={Status.Completed}>Completed</option>
|
||||
<option class="sc-5" value={Status.Failed}>Failed</option>
|
||||
<option class="sc-6" value={Status.Dropped}>Dropped</option>
|
||||
</select>
|
||||
<div class="status-select">
|
||||
{#each STATUS_ENTRIES as [id, iconName] (id)}
|
||||
<div on:click={() => {status = id}} class="option sc-{id}" class:selected={status === id}>
|
||||
<Icon name={iconName} />
|
||||
</div>
|
||||
{/each}
|
||||
<div class="status-name">{STATUS_NAMES[status]}</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
.sc-0
|
||||
color: $sc-0 !important
|
||||
.sc-1
|
||||
color: $sc-1 !important
|
||||
.sc-2
|
||||
color: $sc-2 !important
|
||||
.sc-3
|
||||
color: $sc-3 !important
|
||||
.sc-4
|
||||
color: $sc-4 !important
|
||||
.sc-5
|
||||
color: $sc-5 !important
|
||||
.sc-6
|
||||
color: $sc-6 !important
|
||||
div.status-select
|
||||
background-color: $color-entryhalf
|
||||
display: flex
|
||||
flex-direction: row
|
||||
margin-bottom: 0.8em
|
||||
margin-top: 0.2em
|
||||
|
||||
div.status-name
|
||||
margin-left: auto
|
||||
margin-top: 0.275em
|
||||
margin-right: 1ch
|
||||
color: $color-entry7
|
||||
|
||||
div.option
|
||||
padding: 0.2em 0.5ch
|
||||
font-size: 1.175em
|
||||
color: $color-entry2
|
||||
cursor: pointer
|
||||
line-height: 1em
|
||||
|
||||
&.sc-4, &.sc-2
|
||||
border-left: 1px solid $color-entry1
|
||||
|
||||
&:hover
|
||||
color: $color-entry3
|
||||
|
||||
&.selected.sc-0
|
||||
color: $sc-0 !important
|
||||
&.selected.sc-1
|
||||
color: $sc-1 !important
|
||||
&.selected.sc-2
|
||||
color: $sc-2 !important
|
||||
&.selected.sc-3
|
||||
color: $sc-3 !important
|
||||
&.selected.sc-4
|
||||
color: $sc-4 !important
|
||||
&.selected.sc-5
|
||||
color: $sc-5 !important
|
||||
&.selected.sc-6
|
||||
color: $sc-6 !important
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let value: string[];
|
||||
export let exclaimMode = false;
|
||||
|
||||
let nextTag = "";
|
||||
|
||||
function onKey(ev: KeyboardEvent) {
|
||||
if ((ev.metaKey||ev.ctrlKey) && nextTag === "" && ev.key === "Backspace") {
|
||||
value = value.slice(0, -1);
|
||||
}
|
||||
|
||||
if ((ev.metaKey||ev.ctrlKey) && nextTag !== "" && ev.key === "Enter") {
|
||||
value = [...value, nextTag];
|
||||
nextTag = "";
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
$: while (nextTag.includes(",")) {
|
||||
const newTags = nextTag.split(",").map(t => t.trim());
|
||||
value = [...value, ...newTags.slice(0, -1)];
|
||||
nextTag = newTags[newTags.length - 1];
|
||||
}
|
||||
|
||||
$: value = (value||[]).filter((e, i) => !value.slice(0, i).includes(e));
|
||||
|
||||
$: if (exclaimMode) {
|
||||
value = (value||[]).filter((e, i) => (
|
||||
e.startsWith("!")
|
||||
? !value.slice(i+1).includes(e.slice(1))
|
||||
: !value.slice(i+1).includes("!"+e)
|
||||
));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tag-input">
|
||||
{#each value as tag (tag)}
|
||||
<div class="tag">
|
||||
<span>{tag}</span>
|
||||
<span class="comma">,</span>
|
||||
<span class="x" on:click={() => value = value.filter(v => v !== tag)}>
|
||||
<Icon name="times" />
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
<input placeholder="Add tag (type , or press Ctrl+Enter to add)" on:keyup={onKey} bind:value={nextTag} />
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
div.tag-input
|
||||
width: calc(100% - 2ch)
|
||||
margin-bottom: 1em
|
||||
margin-top: 0.20em
|
||||
min-height: 2em
|
||||
|
||||
background: $color-entryhalf
|
||||
color: $color-entry8
|
||||
border: none
|
||||
outline: none
|
||||
resize: vertical
|
||||
padding: 0.5em 1ch
|
||||
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-wrap: wrap
|
||||
|
||||
> div.tag
|
||||
margin: 0.25em 0.5ch
|
||||
background-color: $color-entry1
|
||||
padding: 0.25em 1ch
|
||||
border-radius: 0.25em
|
||||
|
||||
span.x
|
||||
font-size: 0.75em
|
||||
line-height: 1em
|
||||
user-select: none
|
||||
cursor: pointer
|
||||
|
||||
&:hover
|
||||
color: $color-entry12
|
||||
|
||||
span.comma
|
||||
font-size: 0
|
||||
|
||||
> input
|
||||
margin-bottom: 0.125em
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { formatFormTime } from "$lib/utils/date";
|
||||
import parseInterval from "$lib/utils/timeinterval";
|
||||
import parseInterval, { morningInterval } from "$lib/utils/timeinterval";
|
||||
|
||||
export let from: string;
|
||||
export let to: string;
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
$: {
|
||||
if (intervalName !== "specific_dates") {
|
||||
const int = parseInterval(intervalName, openDate);
|
||||
const int = morningInterval(parseInterval(intervalName, openDate));
|
||||
from = formatFormTime(int.min);
|
||||
to = formatFormTime(int.max);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
<style lang="sass">
|
||||
div.desktop
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
display: none
|
||||
|
||||
div.mobile
|
||||
@media screen and (min-width: 800px)
|
||||
@media screen and (min-width: 750px)
|
||||
display: none
|
||||
</style>
|
||||
@@ -1,10 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { TimeInterval } from "$lib/models/common";
|
||||
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
||||
import { getItemListContext } from "../contexts/ItemListContext.svelte";
|
||||
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||
import HistoryGroupSection from "./HistoryGroupSection.svelte";
|
||||
|
||||
export let intervalString: string;
|
||||
|
||||
const {groups, items} = getItemListContext();
|
||||
const {now} = getTimeContext();
|
||||
|
||||
let interval: TimeInterval<string>
|
||||
$: interval = datesOf(parseInterval(intervalString, $now));
|
||||
</script>
|
||||
|
||||
{#each $groups as group (group.label)}
|
||||
<HistoryGroupSection group={group} items={$items} />
|
||||
{#if (group.label >= interval.min)}
|
||||
<HistoryGroupSection group={group} items={$items} />
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -2,14 +2,19 @@
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemGroup } from "$lib/models/item";
|
||||
import Section from "../layout/Section.svelte";
|
||||
import AggregateAmountRow from "../project/AggregateAmountRow.svelte";
|
||||
import ItemSubSection from "../project/ItemSubSection.svelte";
|
||||
|
||||
export let group: ItemGroup;
|
||||
export let items: Item[];
|
||||
|
||||
let acquiredItems: Item[];
|
||||
$: acquiredItems = group.list.filter(ref => ref.event === "acquired").map(ref => items[ref.idx]);
|
||||
</script>
|
||||
|
||||
<Section title={group.label}>
|
||||
{#each group.list as ref (ref.idx)}
|
||||
<ItemSubSection item={items[ref.idx]} event={ref.event} />
|
||||
{/each}
|
||||
<AggregateAmountRow items={acquiredItems} />
|
||||
</Section>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Icon, { type IconName } from "../layout/Icon.svelte";
|
||||
|
||||
export let name: IconName;
|
||||
export let value: boolean;
|
||||
export let inverse: boolean = false;
|
||||
|
||||
const dispatcher = createEventDispatcher();
|
||||
|
||||
function onClick() {
|
||||
value = !value;
|
||||
dispatcher("click");
|
||||
}
|
||||
</script>
|
||||
|
||||
<span on:click={onClick} class="toggle-icon" class:checked={value !== inverse}>
|
||||
<Icon name={name} />
|
||||
</span>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
.toggle-icon
|
||||
font-size: 0.666em
|
||||
color: $color-entry1
|
||||
user-select: none
|
||||
-moz-user-select: none
|
||||
-webkit-user-select: none
|
||||
|
||||
&.checked
|
||||
color: $color-entry7
|
||||
</style>
|
||||
@@ -14,6 +14,6 @@
|
||||
flex-basis: 50
|
||||
|
||||
&.hideMobile
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
display: none
|
||||
</style>
|
||||
@@ -25,7 +25,7 @@
|
||||
&.fullwidth
|
||||
width: 100%
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
@media screen and (max-width: 749px)
|
||||
width: 100%
|
||||
flex-direction: column
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
|
||||
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
|
||||
|
||||
const icons = {
|
||||
"clock": faClock,
|
||||
@@ -62,6 +65,9 @@
|
||||
"search": faSearch,
|
||||
"chevron_right": faChevronRight,
|
||||
"chevron_down": faChevronDown,
|
||||
"trash": faTrash,
|
||||
"check_slot": faCheckToSlot,
|
||||
"eye": faEye,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -51,21 +51,6 @@
|
||||
div.header
|
||||
display: flex
|
||||
|
||||
h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
color: $color-entry10
|
||||
|
||||
span.sub
|
||||
color: $color-entry5
|
||||
|
||||
&.big
|
||||
margin-top: 1em
|
||||
font-size: 2em
|
||||
|
||||
&.small
|
||||
font-size: 1em
|
||||
|
||||
div.right
|
||||
color: $color-entry2-transparent
|
||||
margin-left: auto
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let centered = false;
|
||||
export let vertical = false;
|
||||
</script>
|
||||
|
||||
<div class="option-row" class:centered>
|
||||
<div class="option-row" class:vertical class:centered>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -10,10 +11,18 @@
|
||||
div.option-row {
|
||||
margin-left: -0.4ch;
|
||||
margin-right: -0.4ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
div.option-row.centered {
|
||||
text-align: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { significantDecimals } from "$lib/utils/numbers";
|
||||
|
||||
export let offset: number;
|
||||
export let integersOnly: boolean = false;
|
||||
|
||||
let value: string;
|
||||
let negative: boolean
|
||||
$: value = (offset > 0) ? `+${offset.toFixed()}` : offset.toFixed();
|
||||
let negative: boolean;
|
||||
$: {
|
||||
if (integersOnly) {
|
||||
value = (offset > 0) ? `+${Math.round(offset).toFixed()}` : Math.round(offset).toFixed();
|
||||
} else {
|
||||
value = (offset > 0) ? `+${significantDecimals(offset)}` : significantDecimals(offset);
|
||||
}
|
||||
}
|
||||
$: negative = offset < 0
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,31 +3,41 @@
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="header-row">
|
||||
{#if title != ""}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
<slot name="right"></slot>
|
||||
<div class="header-col">
|
||||
<slot name="over"></slot>
|
||||
<div class="header-row">
|
||||
{#if title != ""}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
<slot name="under"></slot>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors.sass"
|
||||
|
||||
div.row
|
||||
margin: 0.5em 1ch
|
||||
padding: 0.5em 1ch
|
||||
|
||||
div.header-row
|
||||
display: flex
|
||||
flex-direction: row
|
||||
div.header-col
|
||||
border-bottom: 1px solid $color-entry2-transparent
|
||||
padding-bottom: 0.1em
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
div.header-row
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
> h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
margin-bottom: 0.25em
|
||||
margin-right: auto
|
||||
> h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
margin-bottom: 0.05em
|
||||
margin-right: auto
|
||||
</style>
|
||||
@@ -11,9 +11,10 @@
|
||||
export let noProgress = false;
|
||||
export let icon: IconName = null;
|
||||
export let status: Status = null;
|
||||
export let anchor: string = "";
|
||||
</script>
|
||||
|
||||
<div class="sl3-entry">
|
||||
<div id={anchor} class="sl3-entry">
|
||||
<div class="header">
|
||||
<div class="entry-icon">
|
||||
{#if icon != null}
|
||||
@@ -44,33 +45,20 @@
|
||||
opacity: 0.6
|
||||
margin-right: 0.5ch
|
||||
font-size: 1em
|
||||
margin-top: 0.225em
|
||||
margin-top: 0.15em
|
||||
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
div.header
|
||||
display: flex
|
||||
|
||||
h2
|
||||
font-size: 1.25em
|
||||
margin: 0
|
||||
color: $color-entry10
|
||||
|
||||
span.sub
|
||||
color: $color-entry5
|
||||
|
||||
&.big
|
||||
margin-top: 1em
|
||||
font-size: 2em
|
||||
|
||||
&.small
|
||||
font-size: 1em
|
||||
line-height: 1.25em
|
||||
|
||||
div.right
|
||||
color: $color-entry2-transparent
|
||||
margin-left: auto
|
||||
margin-top: auto
|
||||
margin-bottom: 0
|
||||
|
||||
&.noProgress
|
||||
margin-top: -0.25em
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let icon: IconName = null;
|
||||
export let status: Status = null;
|
||||
export let event: string = "";
|
||||
export let href: string | null = null;
|
||||
|
||||
let blankEvent: boolean
|
||||
|
||||
@@ -29,7 +30,13 @@
|
||||
</StatusColor>
|
||||
{/if}
|
||||
</div>
|
||||
<Title value={title} big={big} small={small} sub={subtitle} />
|
||||
{#if href != null}
|
||||
<a href={href}>
|
||||
<Title value={title} big={big} small={small} sub={subtitle} />
|
||||
</a>
|
||||
{:else}
|
||||
<Title value={title} big={big} small={small} sub={subtitle} />
|
||||
{/if}
|
||||
<div class="right" class:noProgress>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
@@ -43,6 +50,7 @@
|
||||
div.sub-section
|
||||
margin: 0.5em 0.4ch
|
||||
padding: 0.33em 0.75ch
|
||||
padding-bottom: 0.25em
|
||||
background: $color-entry1-transparent
|
||||
border-radius: 0.125em
|
||||
border-bottom-right-radius: 0.5em
|
||||
@@ -50,11 +58,17 @@
|
||||
:global(div.sub-section)
|
||||
background: $color-entry2-transparent
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
opacity: 0.9
|
||||
|
||||
&:hover
|
||||
opacity: 1
|
||||
|
||||
div.entry-icon
|
||||
opacity: 0.6
|
||||
margin-right: 1.5ch
|
||||
font-size: 0.75em
|
||||
margin-top: auto
|
||||
|
||||
span.event
|
||||
font-size: 1.33em
|
||||
@@ -64,7 +78,8 @@
|
||||
display: none
|
||||
|
||||
&.blankEvent
|
||||
margin-right: 0.5ch
|
||||
margin-top: 0.35em
|
||||
margin-right: 0.75ch
|
||||
|
||||
span.event
|
||||
display: none
|
||||
@@ -78,6 +93,6 @@
|
||||
margin-top: auto
|
||||
|
||||
&.noProgress
|
||||
margin-top: -0.25em
|
||||
margin-top: 0em
|
||||
margin-bottom: auto
|
||||
</style>
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
h2
|
||||
font-size: 1.25em
|
||||
line-height: 1.15em
|
||||
margin: 0
|
||||
color: $color-entry10
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type Item from "$lib/models/item";
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
|
||||
export let items: Item[];
|
||||
export let totalAcquired: number = null;
|
||||
export let aggregateRequired: number = null;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
|
||||
let amounts: {name: string, amount: number}[] = [];
|
||||
let total;
|
||||
let required;
|
||||
|
||||
$: {
|
||||
const statMap: Record<number, number> = {};
|
||||
|
||||
total = 0;
|
||||
required = 0;
|
||||
|
||||
for (const item of items) {
|
||||
for (const stat of item.stats) {
|
||||
statMap[stat.id] = (statMap[stat.id] || 0) + stat.acquired;
|
||||
required += stat.required * stat.weight;
|
||||
total += stat.acquired * stat.weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalAcquired != null) {
|
||||
total = totalAcquired;
|
||||
}
|
||||
if (aggregateRequired != null) {
|
||||
required = aggregateRequired;
|
||||
}
|
||||
|
||||
amounts = [];
|
||||
for (const stat of $scope.stats) {
|
||||
if (!!statMap[stat.id]) {
|
||||
amounts.push({name: stat.name, amount: statMap[stat.id]});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AmountRow>
|
||||
{#each amounts as {name, amount} (name)}
|
||||
<Amount label={name} value={amount} />
|
||||
{/each}
|
||||
<Amount weighted right label="" dark value={total} target={required} />
|
||||
</AmountRow>
|
||||
@@ -1,3 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
export type ItemSubSectionEvent = "created" | "scheduled" | "acquired" | "none"
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Markdown from "$lib/components/common/Markdown.svelte";
|
||||
import SubSection from "$lib/components/layout/SubSection.svelte";
|
||||
@@ -8,58 +12,88 @@
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import type { IconName } from "../layout/Icon.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import { formatDays, formatPrettyTime, formatTime } from "$lib/utils/date";
|
||||
import RequirementReference from "./RequirementReference.svelte";
|
||||
import ItemTimeInfo from "./ItemTimeInfo.svelte";
|
||||
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||
import TagRow from "../common/TagRow.svelte";
|
||||
|
||||
const {now} = getTimeContext();
|
||||
|
||||
export let item: Item;
|
||||
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
|
||||
export let event: ItemSubSectionEvent = "none";
|
||||
export let compact = false;
|
||||
export let showAcquiredTime = false;
|
||||
export let hideStats = false;
|
||||
|
||||
let icon: IconName
|
||||
let status: Status
|
||||
let eventText: string
|
||||
|
||||
$: {
|
||||
icon = void(0);
|
||||
|
||||
if (event === "none" || event === "acquired") {
|
||||
if (item.acquiredTime) {
|
||||
icon = "check";
|
||||
status = Status.Completed;
|
||||
eventText = (showAcquiredTime || event === "acquired") ? formatTime(item.acquiredTime) : void(0)
|
||||
}
|
||||
} else if (event === "scheduled") {
|
||||
if (item.acquiredTime) {
|
||||
icon = "hourglass";
|
||||
if (!item.acquiredTime) {
|
||||
icon = "calendar";
|
||||
status = Status.Blocked;
|
||||
eventText = formatDays(new Date(item.scheduledDate), $now)
|
||||
}
|
||||
} else if (event === "created") {
|
||||
icon = "lightbulb";
|
||||
status = Status.Available;
|
||||
icon = "lightbulb";
|
||||
status = Status.Available;
|
||||
eventText = formatTime(item.createdTime)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubSection small noProgress title={item.name} icon={icon} status={status} event={event}>
|
||||
<SubSection small noProgress title={item.name} icon={icon} status={status} event={eventText}>
|
||||
<OptionsRow slot="right">
|
||||
{#if item.acquiredTime == null}
|
||||
<Option open={{name: "item.acquire", item}} color="green">Acquire</Option>
|
||||
<Option open={{name: "item.acquire", item}} color="green"><Icon name="check_slot" /></Option>
|
||||
{/if}
|
||||
{#if !compact}
|
||||
<Option open={{name: "item.edit", item}}>Edit</Option>
|
||||
<Option open={{name: "item.delete", item}} color="red">Delete</Option>
|
||||
<Option open={{name: "item.edit", item}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
|
||||
{/if}
|
||||
</OptionsRow>
|
||||
{#if item.project != null}
|
||||
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
|
||||
{:else}
|
||||
<ItemTimeInfo item={item} />
|
||||
{/if}
|
||||
<TagRow names={item.tags} />
|
||||
{#if !compact}
|
||||
<Markdown source={item.description} />
|
||||
{/if}
|
||||
<AmountRow>
|
||||
{#if item.acquiredTime != null}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
|
||||
{#if !hideStats}
|
||||
{#if item.acquiredTime != null}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount weighted dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.required} />
|
||||
{:else}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount weighted dark right label="" value={item.weightedRequired} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</AmountRow>
|
||||
</SubSection>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type Item from "$lib/models/item";
|
||||
import { formatPrettyTime } from "$lib/utils/date";
|
||||
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||
|
||||
export let item: Item
|
||||
|
||||
const {now} = getTimeContext();
|
||||
|
||||
let action = "create";
|
||||
let colorClass = "";
|
||||
let timeStr = "";
|
||||
|
||||
$: {
|
||||
const ct = new Date(item.createdTime);
|
||||
let t = ct;
|
||||
let noTime = false;
|
||||
|
||||
if (item.acquiredTime != null) {
|
||||
const at = new Date(item.acquiredTime);
|
||||
t = at;
|
||||
|
||||
if (Math.abs(at.getTime() - ct.getTime()) < 300000) {
|
||||
action = "Created/Acquired";
|
||||
} else {
|
||||
action = "Acquired";
|
||||
}
|
||||
colorClass = "acquired";
|
||||
} else if (item.scheduledDate != null) {
|
||||
t = new Date(item.scheduledDate);
|
||||
action = "Scheduled for";
|
||||
colorClass = "scheduled";
|
||||
noTime = true;
|
||||
} else {
|
||||
action = "Created";
|
||||
colorClass = "created";
|
||||
}
|
||||
|
||||
timeStr = formatPrettyTime(t, $now);
|
||||
if (noTime) {
|
||||
timeStr = timeStr.split(" at ")[0];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="item-time {colorClass}">
|
||||
<span class="action">{action}</span>
|
||||
<span class="timeStr">{timeStr}</span>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
div.item-time
|
||||
font-size: 0.8em
|
||||
|
||||
color: $color-entry3
|
||||
|
||||
&.created
|
||||
color: $sc-1-dim
|
||||
&.acquired
|
||||
color: $sc-4-dim
|
||||
&.scheduled
|
||||
color: $sc-0
|
||||
|
||||
> span.timeStr
|
||||
font-weight: 800
|
||||
|
||||
</style>
|
||||
@@ -4,7 +4,9 @@
|
||||
import Main from "$lib/components/layout/Main.svelte";
|
||||
import Option from "$lib/components/layout/Option.svelte";
|
||||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
||||
import TagRow from "../common/TagRow.svelte";
|
||||
import { getProjectContext } from "../contexts/ProjectContext.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import RequirementEntry from "./RequirementSection.svelte";
|
||||
|
||||
const {project} = getProjectContext();
|
||||
@@ -13,11 +15,12 @@
|
||||
<Main big title={$project.name}>
|
||||
<Progress alwaysSmooth titlePercentageOnly thin green status={$project.status} count={$project.totalAcquired} target={$project.totalRequired} />
|
||||
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
|
||||
<TagRow names={$project.tags} />
|
||||
<Markdown source={$project.description} />
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "requirement.create", project: $project}}>Add Requirement</Option>
|
||||
<Option open={{name: "project.edit", project: $project}}>Edit</Option>
|
||||
<Option open={{name: "project.delete", project: $project}} color="red">Delete</Option>
|
||||
<Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>
|
||||
<Option open={{name: "project.edit", project: $project}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "project.delete", project: $project}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
{#each $project.requirements as requirement (requirement.id)}
|
||||
<RequirementEntry requirement={requirement} />
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface IDAndName {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
|
||||
export let project: IDAndName
|
||||
export let requirement: IDAndName
|
||||
export let scopeId: number
|
||||
export let projectOnly: boolean = false;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
let projectLink: string
|
||||
let requirementLink: string
|
||||
|
||||
$: {
|
||||
let actualScope = $scope;
|
||||
if (actualScope == null || actualScope.id !== scopeId) {
|
||||
actualScope = $scopes.find(s => s.id === scopeId);
|
||||
}
|
||||
|
||||
projectLink = `/${scopePrettyId(actualScope)}/projects/${projectPrettyId(project)}`
|
||||
requirementLink = `${projectLink}#${projectPrettyId(requirement)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="reference">
|
||||
<a href={projectLink}>{project.name}</a>
|
||||
{#if !projectOnly}
|
||||
<span>></span>
|
||||
<a href={requirementLink}>{requirement.name}</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors"
|
||||
|
||||
div.reference
|
||||
font-size: 0.8em
|
||||
|
||||
> span
|
||||
color: $color-entry3
|
||||
|
||||
> a
|
||||
color: $color-entry5
|
||||
text-decoration: none
|
||||
line-height: 1em
|
||||
transition: 250ms
|
||||
|
||||
&:hover
|
||||
color: $color-entry8
|
||||
</style>
|
||||
@@ -9,25 +9,37 @@
|
||||
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
|
||||
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
|
||||
import ItemEntry from "./ItemSubSection.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import { projectPrettyId } from "$lib/utils/prettyIds";
|
||||
import AggregateAmountRow from "./AggregateAmountRow.svelte";
|
||||
import TagRow from "../common/TagRow.svelte";
|
||||
|
||||
export let requirement: Requirement;
|
||||
</script>
|
||||
|
||||
<div id={projectPrettyId(requirement)} />
|
||||
<Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
|
||||
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
|
||||
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
|
||||
<TagRow names={requirement.tags} />
|
||||
<Markdown source={requirement.description} />
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "item.create", requirement}}>Add Item</Option>
|
||||
<Option open={{name: "requirement.edit", requirement}}>Edit</Option>
|
||||
<Option open={{name: "requirement.delete", requirement}} color="red">Delete</Option>
|
||||
<Option open={{name: "item.multischedule", items: requirement.items}}><Icon name="calendar" /></Option>
|
||||
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
|
||||
<Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<LabeledProgressRow>
|
||||
{#each requirement.stats as stat (stat.id)}
|
||||
<LabeledProgress count={stat.acquired} target={stat.required} name={stat.name} />
|
||||
{#if !requirement.isCoarse || stat.required > 0}
|
||||
<LabeledProgress count={stat.acquired} target={stat.required} planned={stat.planned} name={stat.name} />
|
||||
{/if}
|
||||
{/each}
|
||||
</LabeledProgressRow>
|
||||
{#each requirement.items as item (item.id)}
|
||||
{#each (requirement.items||[]) as item (item.id)}
|
||||
<ItemEntry item={item} />
|
||||
{/each}
|
||||
{#if requirement.aggregateRequired > 0}
|
||||
<AggregateAmountRow items={requirement.items||[]} totalAcquired={requirement.totalAcquired} aggregateRequired={requirement.aggregateRequired} />
|
||||
{/if}
|
||||
</Section>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import Markdown from "$lib/components/common/Markdown.svelte";
|
||||
import Progress from "$lib/components/common/Progress.svelte";
|
||||
import Option from "$lib/components/layout/Option.svelte";
|
||||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
||||
import type { RequirementWithoutItems } from "$lib/models/project";
|
||||
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import TagRow from "../common/TagRow.svelte";
|
||||
import SubSection from "../layout/SubSection.svelte";
|
||||
import RequirementReference from "./RequirementReference.svelte";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
|
||||
export let requirement: RequirementWithoutItems;
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
let tags: string[]
|
||||
$: {
|
||||
tags = [...requirement.tags, ...(requirement.project?.tags||[])].sort().filter((t, i, a) => a[i-1] !== t);
|
||||
}
|
||||
|
||||
let projectLink: string
|
||||
let requirementLink: string
|
||||
$: {
|
||||
let actualScope = $scope;
|
||||
if (actualScope == null || actualScope.id !== requirement.project.scopeId) {
|
||||
actualScope = $scopes.find(s => s.id === requirement.project.scopeId);
|
||||
}
|
||||
|
||||
projectLink = `/${scopePrettyId(actualScope)}/projects/${projectPrettyId(requirement.project)}`
|
||||
requirementLink = `${projectLink}#${projectPrettyId(requirement)}`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div id={projectPrettyId(requirement)} />
|
||||
<SubSection href={requirementLink} small title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
|
||||
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
|
||||
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
|
||||
<RequirementReference projectOnly project={requirement.project} requirement={requirement} scopeId={requirement.project.scopeId} />
|
||||
<TagRow names={tags} />
|
||||
<Markdown source={requirement.description} />
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
|
||||
<Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
</SubSection>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { addDays, endOfDay, formatWeekdayDate, formatWeekdayTitle, startOfDay } from "$lib/utils/date";
|
||||
import { significantDecimals } from "$lib/utils/numbers";
|
||||
import { morningOf } from "$lib/utils/timeinterval";
|
||||
import { getItemMultiListContext } from "../contexts/ItemMultiListContext.svelte";
|
||||
import { getTimeContext } from "../contexts/TimeContext.svelte";
|
||||
import Row from "../layout/Row.svelte";
|
||||
import ItemSubSection, { type ItemSubSectionEvent } from "../project/ItemSubSection.svelte";
|
||||
|
||||
export let key: string;
|
||||
export let title: string;
|
||||
export let showAcquiredTime: boolean = false;
|
||||
export let useAcquiredTotal: boolean = false;
|
||||
export let useDayFilter: number | null = null;
|
||||
export let selectedDay: number = 0;
|
||||
export let event: ItemSubSectionEvent = "none";
|
||||
|
||||
const {now} = getTimeContext();
|
||||
const {lists} = getItemMultiListContext();
|
||||
|
||||
let dayTitle = "";
|
||||
let filteredList = [];
|
||||
$: {
|
||||
if (useDayFilter != null) {
|
||||
const maxTime = addDays(morningOf($now), -(selectedDay - 1)).toISOString();
|
||||
const minTime = addDays(morningOf($now), -selectedDay).toISOString();
|
||||
filteredList = $lists[key].filter(i => i.acquiredTime >= minTime && i.acquiredTime <= maxTime)
|
||||
dayTitle = formatWeekdayTitle(morningOf($now), new Date(minTime))
|
||||
} else {
|
||||
filteredList = $lists[key] || [];
|
||||
}
|
||||
}
|
||||
|
||||
let totalStr = "";
|
||||
$: {
|
||||
totalStr = significantDecimals(filteredList.map(v => useAcquiredTotal ? v.weightedAcquired : v.weightedRequired).reduce((p,c) => p + c, 0));
|
||||
if (Number(totalStr) < 0.01) {
|
||||
totalStr = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if (filteredList.length > 0 || useDayFilter != null)}
|
||||
<Row title={title || dayTitle}>
|
||||
<div class="total" slot="right">{totalStr}</div>
|
||||
<div class="day-filters" slot="over">
|
||||
{#if (useDayFilter != null)}
|
||||
{#each {length: useDayFilter} as _, i}
|
||||
<div on:click={() => selectedDay = i} class="option" class:selected={i === selectedDay}>
|
||||
<div class="inner"></div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#each filteredList as item (item.id)}
|
||||
<ItemSubSection event={event} showAcquiredTime={showAcquiredTime} item={item} />
|
||||
{/each}
|
||||
</Row>
|
||||
{/if}
|
||||
|
||||
<style lang="sass">
|
||||
@import "../../css/colors.sass"
|
||||
|
||||
div.total
|
||||
color: $color-entry5
|
||||
margin-top: 0.25em
|
||||
|
||||
div.day-filters
|
||||
display: flex
|
||||
flex-direction: row
|
||||
max-width: 100%
|
||||
flex-wrap: wrap
|
||||
|
||||
div.option
|
||||
padding: 0.1em 0.25em
|
||||
cursor: pointer
|
||||
|
||||
&:first-child
|
||||
margin-left: -0.25em
|
||||
@media screen and (max-width: 749px)
|
||||
margin-left: 0
|
||||
|
||||
> div.inner
|
||||
width: 1.25ch
|
||||
height: 1.25ch
|
||||
border: 1px solid $color-entry2
|
||||
|
||||
@media screen and (max-width: 749px)
|
||||
width: 2.5ch
|
||||
height: 2.5ch
|
||||
|
||||
|
||||
&.selected > div.inner
|
||||
border-color: $color-entry7
|
||||
background-color: $color-entry7
|
||||
|
||||
&:hover > div.inner
|
||||
border-color: $color-entry9
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
import Status from "$lib/models/status";
|
||||
|
||||
const STATUS_ORDER = [
|
||||
export const STATUS_ORDER = [
|
||||
Status.Active,
|
||||
Status.Background,
|
||||
Status.Available,
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import { SprintKind } from "$lib/models/sprint";
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import BurndownChart from "../common/BurndownChart.svelte";
|
||||
import LabeledProgress from "../common/LabeledProgress.svelte";
|
||||
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
|
||||
import Markdown from "../common/Markdown.svelte";
|
||||
import SprintKindSelect from "../controls/SprintKindSelect.svelte";
|
||||
import ItemSubSection from "../project/ItemSubSection.svelte";
|
||||
|
||||
export let sprint: Sprint
|
||||
export let compact: boolean = false;
|
||||
|
||||
let timeRange: [Date, Date];
|
||||
|
||||
@@ -17,13 +20,22 @@
|
||||
} else {
|
||||
timeRange = void(0);
|
||||
}
|
||||
</script>
|
||||
|
||||
let aggregateRequired = 0;
|
||||
let aggregateSmooth = false;
|
||||
$: if (sprint.kind === SprintKind.Stats && sprint.aggregateRequired < 1) {
|
||||
aggregateSmooth = true;
|
||||
aggregateRequired = sprint.progress.reduce((a, c) => a + (c.weight * c.required), 0);
|
||||
} else {
|
||||
aggregateSmooth = false;
|
||||
aggregateRequired = sprint.aggregateRequired;
|
||||
}
|
||||
</script>
|
||||
<Markdown source={sprint.description} />
|
||||
{#if sprint.kind === SprintKind.Items}
|
||||
<LabeledProgress timeRange={timeRange} green name={sprint.aggregateName || "Items"} count={sprint.itemsAcquired} target={sprint.itemsRequired} />
|
||||
{:else}
|
||||
<LabeledProgress timeRange={timeRange} name={sprint.aggregateName} count={sprint.aggregateAcquired} target={sprint.aggregateRequired} />
|
||||
<LabeledProgress alwaysSmooth={aggregateSmooth} timeRange={timeRange} name={sprint.aggregateName} count={sprint.aggregateAcquired} target={aggregateRequired} weighted={!sprint.isUnweighted} />
|
||||
{/if}
|
||||
{#if !sprint.isCoarse}
|
||||
<LabeledProgressRow>
|
||||
@@ -32,14 +44,26 @@
|
||||
{/each}
|
||||
</LabeledProgressRow>
|
||||
{/if}
|
||||
{#if sprint.kind !== SprintKind.Items && sprint.aggregateBurndown != null && sprint.aggregateBurndown.length > 0}
|
||||
<BurndownChart data={sprint.aggregateBurndown} from={sprint.fromTime} to={sprint.toTime} milestone={aggregateRequired||1} />
|
||||
{/if}
|
||||
{#if sprint.kind === SprintKind.Items && sprint.itemBurndown != null && sprint.itemBurndown.length > 0}
|
||||
<BurndownChart green data={sprint.itemBurndown} from={sprint.fromTime} to={sprint.toTime} milestone={sprint.itemsRequired||1} />
|
||||
{/if}
|
||||
{#if sprint.kind === SprintKind.Items}
|
||||
{#each (sprint.items||[]) as item (item.id)}
|
||||
{#if !item.acquiredTime}
|
||||
<ItemSubSection compact item={item} />
|
||||
{#if !compact || !item.acquiredTime}
|
||||
<ItemSubSection compact={compact} hideStats={compact} item={item} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<AmountRow>
|
||||
<Amount label="Acquired Items" value={(sprint.items||[]).length} />
|
||||
</AmountRow>
|
||||
{#if compact}
|
||||
<AmountRow>
|
||||
<Amount label="Acquired Items" value={(sprint.items||[]).length} />
|
||||
</AmountRow>
|
||||
{:else}
|
||||
{#each (sprint.items||[]) as item (item.id)}
|
||||
<ItemSubSection compact item={item} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,27 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
|
||||
import Option from "../layout/Option.svelte";
|
||||
import OptionsRow from "../layout/OptionsRow.svelte";
|
||||
import SprintBody from "./SprintBody.svelte";
|
||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
import { projectPrettyId } from "$lib/utils/prettyIds";
|
||||
import Section from "../layout/Section.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let sprint: Sprint;
|
||||
const {scope} = getScopeContext();
|
||||
|
||||
function gotoSprint() {
|
||||
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section noProgress title={sprint.name}>
|
||||
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
|
||||
<OptionsRow slot="right">
|
||||
<Option on:click={gotoSprint}>View</Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintBody sprint={sprint} />
|
||||
</Section>
|
||||
@@ -8,20 +8,28 @@
|
||||
import SprintBody from "./SprintBody.svelte";
|
||||
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import { getScopeContext } from "../contexts/ScopeContext.svelte";
|
||||
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
|
||||
export let sprint: Sprint;
|
||||
const {scope} = getScopeContext();
|
||||
const {scope, lastHistoryRange} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
|
||||
function gotoSprint() {
|
||||
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
|
||||
let sc = $scope;
|
||||
if (sc.id !== sprint.scopeId) {
|
||||
sc = $scopes.find(s => s.id === sprint.scopeId);
|
||||
}
|
||||
|
||||
goto(`/${scopePrettyId(sc)}/sprints/${$lastHistoryRange}#${projectPrettyId(sprint)}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubSection noProgress title={sprint.name}>
|
||||
<OptionsRow slot="right">
|
||||
<Option on:click={gotoSprint}>View</Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
|
||||
<Option on:click={gotoSprint}><Icon name="eye" /></Option>
|
||||
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintBody sprint={sprint} />
|
||||
<SprintBody compact sprint={sprint} />
|
||||
</SubSection>
|
||||
@@ -3,6 +3,7 @@
|
||||
import Amount from "../common/Amount.svelte";
|
||||
import AmountRow from "../common/AmountRow.svelte";
|
||||
import Markdown from "../common/Markdown.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import Option from "../layout/Option.svelte";
|
||||
import OptionsRow from "../layout/OptionsRow.svelte";
|
||||
import SubSection from "../layout/SubSection.svelte";
|
||||
@@ -12,13 +13,13 @@
|
||||
|
||||
<SubSection noProgress title={stat.name}>
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "stat.edit", stat}}>Edit</Option>
|
||||
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
|
||||
<Option open={{name: "stat.edit", stat}}><Icon name="pen" /></Option>
|
||||
<Option open={{name: "stat.delete", stat}} color="red"><Icon name="trash" /></Option>
|
||||
</OptionsRow>
|
||||
<Markdown source={stat.description} />
|
||||
{#if stat.weight !== 1}
|
||||
<AmountRow>
|
||||
<Amount label="Weight" value={stat.weight} />
|
||||
<Amount weighted label="Weight" value={stat.weight} />
|
||||
</AmountRow>
|
||||
{/if}
|
||||
</SubSection>
|
||||
@@ -25,9 +25,11 @@ $opacity-entry6: 0.80
|
||||
$sc-0: #78c9ff
|
||||
$sc-0-dark: #34424b
|
||||
$sc-1: #e7e55e
|
||||
$sc-1-dim: #a3a352
|
||||
$sc-2: #e7a75e
|
||||
$sc-3: $color-entry12
|
||||
$sc-4: #78ff78
|
||||
$sc-4-dim: #549454
|
||||
$sc-4-dark: #2f422f
|
||||
$sc-5: #c94339
|
||||
$sc-6: #e75ed0
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type Stat from "$lib/models/stat";
|
||||
import { scopePrettyId } from "$lib/utils/prettyIds";
|
||||
|
||||
@@ -26,6 +29,8 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
const {reloadProjectList} = getProjectListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let endpoint: string;
|
||||
|
||||
@@ -49,7 +54,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
break;
|
||||
|
||||
case "item.delete":
|
||||
init(`items/${$currentModal.item.id}`)
|
||||
init(`items/${$currentModal.item.id}`, [], $currentModal.item.scopeId)
|
||||
noun = "Item";
|
||||
name = $currentModal.item.name;
|
||||
break;
|
||||
@@ -62,17 +67,18 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
break;
|
||||
|
||||
case "sprint.delete":
|
||||
init(`sprints/${$currentModal.sprint.id}`)
|
||||
init(`sprints/${$currentModal.sprint.id}`, [], $currentModal.sprint.scopeId)
|
||||
noun = "Sprint";
|
||||
name = $currentModal.sprint.name;
|
||||
break;
|
||||
|
||||
case "requirement.delete":
|
||||
init(`projects/${$project.id}/requirements/${$currentModal.requirement.id}`,
|
||||
$currentModal.requirement.items.map(i => ({
|
||||
init(`projects/${$currentModal.requirement.project?.id || $project.id}/requirements/${$currentModal.requirement.id}`,
|
||||
(($currentModal.requirement as any).items||[]).map((i: Item) => ({
|
||||
op: "Detach item",
|
||||
name: i.name,
|
||||
}))
|
||||
})),
|
||||
$currentModal.requirement.project?.scopeId || void(0)
|
||||
);
|
||||
noun = "Requirement";
|
||||
name = $currentModal.requirement.name;
|
||||
@@ -100,8 +106,13 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
show = false;
|
||||
}
|
||||
|
||||
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = []) {
|
||||
endpoint = `scopes/${$scope.id}/${newEndpoint}`;
|
||||
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = [], overrideScopeId?: number) {
|
||||
let scopeId = $scope.id;
|
||||
if (overrideScopeId != null) {
|
||||
scopeId = overrideScopeId;
|
||||
}
|
||||
|
||||
endpoint = `scopes/${scopeId}/${newEndpoint}`;
|
||||
if (endpoint.endsWith("/")) {
|
||||
endpoint = endpoint.slice(0, -1);
|
||||
}
|
||||
@@ -131,6 +142,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
await reloadProject();
|
||||
|
||||
if (noun === "Stat") {
|
||||
reloadItemLists();
|
||||
deleteStat((res as {stat: Stat}).stat);
|
||||
}
|
||||
|
||||
@@ -139,6 +151,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
|
||||
}
|
||||
|
||||
if (noun === "Item") {
|
||||
reloadItemLists();
|
||||
await reloadItemList();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
|
||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemInput } from "$lib/models/item";
|
||||
import type Scope from "$lib/models/scope";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {scope} = getScopeContext();
|
||||
const {reloadProject} = getProjectContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let item: Partial<ItemInput>
|
||||
let itemId: number
|
||||
let scopeId: number
|
||||
let requirementId: number
|
||||
let openedDate: Date
|
||||
|
||||
@@ -31,7 +33,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
|
||||
$: switch ($currentModal.name) {
|
||||
case "item.acquire":
|
||||
init($scope, $currentModal.item)
|
||||
init($currentModal.item)
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -40,7 +42,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
show = false;
|
||||
}
|
||||
|
||||
function init(scope: Scope, unaquiredItem: Item) {
|
||||
function init(unaquiredItem: Item) {
|
||||
openedDate = new Date();
|
||||
|
||||
item = {
|
||||
@@ -50,6 +52,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
}
|
||||
|
||||
show = true;
|
||||
scopeId = unaquiredItem.scopeId;
|
||||
itemId = unaquiredItem.id;
|
||||
requirementId = unaquiredItem.requirementId;
|
||||
}
|
||||
@@ -59,7 +62,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await sl3(fetch).updateItem($scope.id, itemId, {
|
||||
await sl3(fetch).updateItem(scopeId, itemId, {
|
||||
...item,
|
||||
acquiredTime: new Date(item.acquiredTime).toISOString(),
|
||||
});
|
||||
@@ -69,6 +72,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
await reloadProject();
|
||||
}
|
||||
|
||||
reloadItemLists();
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
|
||||
@@ -94,7 +98,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={item.description} />
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showAcquired hideUnseen noToggle bind:value={item.stats} />
|
||||
<StatInput overrideScopeId={scopeId} showAcquired hideUnseen noToggle bind:value={item.stats} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -1,57 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
|
||||
import RequirementSelect from "$lib/components/controls/RequirementSelect.svelte";
|
||||
import ScheduledDateInput from "$lib/components/controls/ScheduledDateInput.svelte";
|
||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||
import TagInput from "$lib/components/controls/TagInput.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemInput } from "$lib/models/item";
|
||||
import type { Requirement } from "$lib/models/project";
|
||||
import type { Requirement, RequirementWithoutItems } from "$lib/models/project";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import { formatFormTime } from "$lib/utils/date";
|
||||
import { statDiff } from "$lib/utils/stat";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {scope} = getScopeContext();
|
||||
const {scopes} = getScopeListContext();
|
||||
const {project, reloadProject} = getProjectContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
|
||||
let item: ItemInput
|
||||
let itemId: number
|
||||
let scopeId: number
|
||||
let currentStats: ItemInput["stats"]
|
||||
let openedDate: Date
|
||||
let requirementName: string
|
||||
let op: string
|
||||
let initOnList: number[] = [];
|
||||
let oldTags: string[] = [];
|
||||
|
||||
let error: string
|
||||
let loading: boolean
|
||||
let show: boolean
|
||||
|
||||
$: switch ($currentModal.name) {
|
||||
case "item.create":
|
||||
initCreate($scope, $currentModal.requirement)
|
||||
break;
|
||||
case "item.edit":
|
||||
initEdit($currentModal.item)
|
||||
break;
|
||||
|
||||
default:
|
||||
loading = false;
|
||||
error = null;
|
||||
show = false;
|
||||
let addAnother: boolean
|
||||
let nameElem: HTMLElement
|
||||
|
||||
$: if (!show) {
|
||||
switch ($currentModal.name) {
|
||||
case "item.create":
|
||||
initCreate($scope, $currentModal.requirement)
|
||||
break;
|
||||
case "item.edit": {
|
||||
const reqId = $currentModal.item.requirementId;
|
||||
initEdit($currentModal.item, $project.requirements.find(r => r.id === reqId), $scope)
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!$currentModal.name?.startsWith("item.")) {
|
||||
loading = false;
|
||||
error = null;
|
||||
show = false;
|
||||
addAnother = false;
|
||||
}
|
||||
|
||||
function initCreate(scope: Scope, requirement?: Requirement) {
|
||||
function initCreate(scope: Scope, requirement?: RequirementWithoutItems) {
|
||||
let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
|
||||
if (stats == null) {
|
||||
stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
|
||||
} else {
|
||||
initOnList = requirement.stats.filter(s => s.required > 0).map(s => s.id);
|
||||
}
|
||||
|
||||
item = {
|
||||
@@ -61,32 +81,48 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
stats: stats,
|
||||
acquiredTime: null,
|
||||
scheduledDate: null,
|
||||
tags: [],
|
||||
}
|
||||
|
||||
op = "Create"
|
||||
scopeId = requirement?.project?.scopeId || scope?.id;
|
||||
openedDate = new Date();
|
||||
requirementName = requirement?.name;
|
||||
show = true;
|
||||
}
|
||||
|
||||
function initEdit(current: Item) {
|
||||
function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
|
||||
const req = $project.requirements?.find(r => r.id === current.requirementId);
|
||||
let ctxStats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
|
||||
if (ctxStats == null || ctxStats.length === 0) {
|
||||
let actualScope = scope;
|
||||
if (actualScope.id !== current.scopeId && $scopes?.length > 0) {
|
||||
actualScope = $scopes.find(s => s.id === current.scopeId)
|
||||
}
|
||||
|
||||
ctxStats = actualScope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
|
||||
}
|
||||
const inputStats = current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required}));
|
||||
|
||||
item = {
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
requirementId: current.requirementId,
|
||||
stats: current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required})),
|
||||
stats: ctxStats.map(s => inputStats.find(s2 => s2.statId === s.statId) || s),
|
||||
acquiredTime: formatFormTime(current.acquiredTime),
|
||||
scheduledDate: current.scheduledDate,
|
||||
tags: [...current.tags],
|
||||
};
|
||||
itemId = current.id;
|
||||
scopeId = current.scopeId;
|
||||
currentStats = [...item.stats];
|
||||
|
||||
op = "Edit"
|
||||
openedDate = new Date();
|
||||
requirementName = req?.name;
|
||||
requirementName = current.requirement?.name;
|
||||
show = true;
|
||||
|
||||
oldTags = [...current.tags];
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -96,7 +132,9 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
const submission: ItemInput = {
|
||||
...item,
|
||||
acquiredTime: item.acquiredTime ? new Date(item.acquiredTime).toISOString() : void(0),
|
||||
stats: item.stats.filter(s => s.required > 0),
|
||||
scheduledDate: item.scheduledDate || void(0),
|
||||
clearScheduledDate: !item.scheduledDate,
|
||||
stats: item.stats.filter(s => s.required > 0).map(s => ({...s})),
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -112,7 +150,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
}
|
||||
}
|
||||
|
||||
await sl3(fetch).createItem($scope.id, submission);
|
||||
await sl3(fetch).createItem(scopeId, submission);
|
||||
break;
|
||||
case "Edit":
|
||||
if (!submission.acquiredTime) {
|
||||
@@ -122,8 +160,13 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
submission.clearAcquiredTime = true;
|
||||
}
|
||||
|
||||
submission.addTags = submission.tags.filter(t => !oldTags.includes(t));
|
||||
submission.removeTags = oldTags.filter(t => !submission.tags.includes(t));
|
||||
|
||||
delete submission.tags
|
||||
|
||||
submission.stats = statDiff(currentStats, submission.stats)
|
||||
await sl3(fetch).updateItem($scope.id, itemId, submission);
|
||||
await sl3(fetch).updateItem(scopeId, itemId, submission);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -134,8 +177,15 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
reloadItemLists();
|
||||
|
||||
closeModal();
|
||||
if (addAnother) {
|
||||
item.name = "";
|
||||
item.description = "";
|
||||
nameElem.focus();
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
} catch(err) {
|
||||
if (err.statusCode != null) {
|
||||
error = err.statusMessage;
|
||||
@@ -153,19 +203,37 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
|
||||
<Modal wide closable show={show} verb={op} noun="Item" disabled={loading} error={error}>
|
||||
<ModalBody>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={item.name} />
|
||||
<input bind:this={nameElem} name="name" type="text" bind:value={item.name} />
|
||||
<label for="acquiredTime">Acquired</label>
|
||||
<AcquiredTimeInput openDate={openedDate} bind:value={item.acquiredTime} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={item.description} />
|
||||
<label for="scheduledTime">Scheduled</label>
|
||||
<ScheduledDateInput openDate={openedDate} bind:value={item.scheduledDate} />
|
||||
<label for="tags">Tags</label>
|
||||
<TagInput bind:value={item.tags} />
|
||||
{#if op === "Create"}
|
||||
<Checkbox bind:checked={addAnother} label="Add another." />
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalBody>
|
||||
{#if requirementName != null}
|
||||
{#if $project.id != null || requirementName != null}
|
||||
<label for="req">Project Requirement</label>
|
||||
<input disabled name="req" type="text" value={requirementName} />
|
||||
{#if $project.id != null}
|
||||
<RequirementSelect disabled={op === "Create"} requirements={$project.requirements} bind:value={item.requirementId} />
|
||||
{:else if requirementName != null}
|
||||
<input disabled name="req" type="text" value={requirementName} />
|
||||
{/if}
|
||||
{/if}
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showAcquired={op === "Edit" && !!item.acquiredTime} showRequired hideUnseen={!!item.requirementId} bind:value={item.stats} />
|
||||
<StatInput
|
||||
zeroDeletes showRequired
|
||||
overrideScopeId={scopeId}
|
||||
initOnList={initOnList}
|
||||
showAcquired={op === "Edit" && !!item.acquiredTime}
|
||||
hideUnseen={!!item.requirementId}
|
||||
bind:value={item.stats}
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import ScheduledDateInput from "$lib/components/controls/ScheduledDateInput.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {reloadProject} = getProjectContext();
|
||||
const {reloadItemList} = getItemListContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {reloadItemLists} = getItemMultiListContext();
|
||||
|
||||
let items: Item[]
|
||||
let openedDate: Date
|
||||
let scheduleDate: string;
|
||||
|
||||
let error: string
|
||||
let loading: boolean
|
||||
let show: boolean
|
||||
|
||||
$: switch ($currentModal.name) {
|
||||
case "item.multischedule":
|
||||
init($currentModal.items)
|
||||
break;
|
||||
|
||||
default:
|
||||
loading = false;
|
||||
error = null;
|
||||
show = false;
|
||||
}
|
||||
|
||||
function init(selectedItems: Item[]) {
|
||||
openedDate = new Date();
|
||||
items = selectedItems.map(i => ({...i}));
|
||||
show = true;
|
||||
scheduleDate = selectedItems.find(i => i.scheduledDate)?.scheduledDate || "";
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
for (const item of items) {
|
||||
await sl3(fetch).updateItem(item.scopeId, item.id, {
|
||||
scheduledDate: scheduleDate || void(0),
|
||||
clearScheduledDate: scheduleDate === "",
|
||||
});
|
||||
}
|
||||
|
||||
reloadItemLists();
|
||||
|
||||
await reloadProject();
|
||||
await reloadItemList();
|
||||
await reloadSprintList();
|
||||
|
||||
closeModal();
|
||||
} catch(err) {
|
||||
if (err.statusCode != null) {
|
||||
error = err.statusMessage;
|
||||
} else {
|
||||
error = err
|
||||
}
|
||||
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<Modal closable show={show} verb="Schedule" noun="Items" disabled={loading} error={error}>
|
||||
<ModalBody>
|
||||
<label for="stats">Date</label>
|
||||
<ScheduledDateInput notScheduledText="Clear Scheduled Date" openDate={openedDate} bind:value={scheduleDate} />
|
||||
<p>This changes the scheduled date on all items in the requirement, regardless of whether they already have a scheduled date.</p>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { goto } from "$app/navigation";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
@@ -10,6 +10,7 @@ import { goto } from "$app/navigation";
|
||||
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import StatusSelect from "$lib/components/controls/StatusSelect.svelte";
|
||||
import TagInput from "$lib/components/controls/TagInput.svelte";
|
||||
import type Project from "$lib/models/project";
|
||||
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project";
|
||||
import Status from "$lib/models/status";
|
||||
@@ -19,10 +20,12 @@ import { goto } from "$app/navigation";
|
||||
const {scope} = getScopeContext();
|
||||
const {reloadProject} = getProjectContext();
|
||||
const {reloadProjectList} = getProjectListContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let project: ProjectInput
|
||||
let projectId: number
|
||||
let oldStats: RequirementInput["stats"]
|
||||
let oldTags: string[]
|
||||
|
||||
let error: string
|
||||
let loading: boolean
|
||||
@@ -52,6 +55,7 @@ import { goto } from "$app/navigation";
|
||||
description: "",
|
||||
status: Status.Available,
|
||||
createdTime: void(0),
|
||||
tags: [],
|
||||
}
|
||||
|
||||
show = true;
|
||||
@@ -62,8 +66,10 @@ import { goto } from "$app/navigation";
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
status: current.status,
|
||||
tags: [...current.tags],
|
||||
}
|
||||
|
||||
oldTags = current.tags;
|
||||
projectId = current.id;
|
||||
show = true;
|
||||
}
|
||||
@@ -79,7 +85,14 @@ import { goto } from "$app/navigation";
|
||||
projectId = newProject.id
|
||||
break;
|
||||
case "Edit":
|
||||
await sl3(fetch).updateProject($scope.id, projectId, project);
|
||||
const submission = {
|
||||
...project,
|
||||
addTags: project.tags.filter(t => !oldTags.includes(t)),
|
||||
removeTags: oldTags.filter(t => !project.tags.includes(t)),
|
||||
tags: void(0),
|
||||
}
|
||||
|
||||
await sl3(fetch).updateProject($scope.id, projectId, submission);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -112,6 +125,7 @@ import { goto } from "$app/navigation";
|
||||
<textarea name="description" bind:value={project.description} />
|
||||
<label for="stats">Status</label>
|
||||
<StatusSelect bind:status={project.status} />
|
||||
<TagInput bind:value={project.tags} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -1,16 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import ProjectSelect from "$lib/components/controls/ProjectSelect.svelte";
|
||||
import StatInput from "$lib/components/controls/StatInput.svelte";
|
||||
import StatusSelect from "$lib/components/controls/StatusSelect.svelte";
|
||||
import type { ProjectEntry, Requirement, RequirementInput } from "$lib/models/project";
|
||||
import TagInput from "$lib/components/controls/TagInput.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import type { ProjectEntry, RequirementInput, RequirementWithoutItems } from "$lib/models/project";
|
||||
import Status from "$lib/models/status";
|
||||
import { statDiff } from "$lib/utils/stat";
|
||||
import { statDiff } from "$lib/utils/stat";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {scope} = getScopeContext();
|
||||
@@ -20,6 +22,9 @@ import { statDiff } from "$lib/utils/stat";
|
||||
let projectId: number
|
||||
let requirementId: number
|
||||
let oldStats: RequirementInput["stats"]
|
||||
let showAggregateRequired: boolean
|
||||
let oldTags: string[]
|
||||
let scopeId: number
|
||||
|
||||
let error: string
|
||||
let loading: boolean
|
||||
@@ -43,29 +48,41 @@ import { statDiff } from "$lib/utils/stat";
|
||||
show = false;
|
||||
}
|
||||
|
||||
$: showAggregateRequired = requirement?.stats?.find(s => s.required > 0) == null;
|
||||
|
||||
function initCreate(project: ProjectEntry) {
|
||||
requirement = {
|
||||
name: "",
|
||||
description: "",
|
||||
stats: [],
|
||||
status: Status.Available,
|
||||
isCoarse: false,
|
||||
aggregateRequired: 0,
|
||||
tags: [],
|
||||
projectId: project.id,
|
||||
}
|
||||
|
||||
projectId = project.id;
|
||||
show = true;
|
||||
}
|
||||
|
||||
function initEdit(prev: Requirement, project: ProjectEntry) {
|
||||
function initEdit(current: RequirementWithoutItems, project: ProjectEntry) {
|
||||
requirement = {
|
||||
name: prev.name,
|
||||
description: prev.description,
|
||||
status: prev.status,
|
||||
stats: prev.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
||||
name: current.name,
|
||||
description: current.description,
|
||||
status: current.status,
|
||||
stats: current.stats.map(s => ({acquired: s.acquired, required: s.required, statId: s.id})),
|
||||
isCoarse: current.isCoarse,
|
||||
aggregateRequired: current.aggregateRequired,
|
||||
tags: [...current.tags],
|
||||
projectId: project.id,
|
||||
}
|
||||
|
||||
oldTags = [...current.tags];
|
||||
oldStats = [...requirement.stats];
|
||||
projectId = project.id;
|
||||
requirementId = prev.id;
|
||||
projectId = current.project?.id || project.id;
|
||||
requirementId = current.id;
|
||||
scopeId = current.project?.scopeId || $scope?.id;
|
||||
show = true;
|
||||
}
|
||||
|
||||
@@ -73,6 +90,10 @@ import { statDiff } from "$lib/utils/stat";
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
if (requirement.stats.find(s => s.required > 0)) {
|
||||
requirement.aggregateRequired = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (op) {
|
||||
case "Create":
|
||||
@@ -81,10 +102,13 @@ import { statDiff } from "$lib/utils/stat";
|
||||
case "Edit":
|
||||
const submission: Partial<RequirementInput> = {
|
||||
...requirement,
|
||||
stats: statDiff(oldStats, requirement.stats, -1)
|
||||
stats: statDiff(oldStats, requirement.stats, -1),
|
||||
addTags: requirement.tags.filter(t => !oldTags.includes(t)),
|
||||
removeTags: oldTags.filter(t => !requirement.tags.includes(t)),
|
||||
projectId: requirement.projectId !== projectId ? requirement.projectId : void(0),
|
||||
};
|
||||
|
||||
await sl3(fetch).updateRequirement($scope.id, projectId, requirementId, submission);
|
||||
await sl3(fetch).updateRequirement(scopeId || $scope.id, projectId, requirementId, submission);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -112,12 +136,20 @@ import { statDiff } from "$lib/utils/stat";
|
||||
<input name="name" type="text" bind:value={requirement.name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={requirement.description} />
|
||||
{#if showAggregateRequired}
|
||||
<label for="aggregateRequired">Aggregate Required (Zero for old behavior)</label>
|
||||
<input name="aggregateRequired" type="number" bind:value={requirement.aggregateRequired} />
|
||||
{/if}
|
||||
<TagInput bind:value={requirement.tags} />
|
||||
</ModalBody>
|
||||
<ModalBody>
|
||||
<label for="req">Project Requirement</label>
|
||||
<ProjectSelect disabled={op === "Create"} bind:value={requirement.projectId} />
|
||||
<label for="stats">Status</label>
|
||||
<StatusSelect bind:status={requirement.status} />
|
||||
<label for="stats">Stats</label>
|
||||
<StatInput showRequired bind:value={requirement.stats} />
|
||||
<StatInput overrideScopeId={scopeId} showRequired bind:value={requirement.stats} />
|
||||
<Checkbox bind:checked={requirement.isCoarse} label="Hide 0-requirement stats." />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getRequirementListContext } from "$lib/components/contexts/RequirementListContext.svelte";
|
||||
import RequirementSubSection from "$lib/components/project/RequirementSubSection.svelte";
|
||||
import type { RequirementWithoutItems } from "$lib/models/project";
|
||||
import Status from "$lib/models/status";
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {requirements, reloadRequirementList} = getRequirementListContext();
|
||||
|
||||
let error: string
|
||||
let loading: boolean;
|
||||
let show: boolean;
|
||||
let search: string = "";
|
||||
let searchRaw: string = "";
|
||||
let searchBar: HTMLInputElement;
|
||||
let filteredRequirements: RequirementWithoutItems[] = [];
|
||||
let searchTimeout: any = null;
|
||||
|
||||
$: if ($currentModal.name !== "closed") {
|
||||
show = false;
|
||||
}
|
||||
|
||||
$: {
|
||||
clearTimeout(searchTimeout);
|
||||
let newSearch = searchRaw.slice();
|
||||
searchTimeout = setTimeout(() => {
|
||||
search = newSearch;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$: {
|
||||
filteredRequirements = $requirements.filter(r => r.status > Status.Blocked && r.status < Status.Completed);
|
||||
if (search.length > 0) {
|
||||
const scoreMap = new Map<number, number>();
|
||||
const searchLc = search.toLocaleLowerCase();
|
||||
const searchWords = searchLc.split(" ");
|
||||
|
||||
for (const req of filteredRequirements) {
|
||||
const projectNameLc = req.project.name?.toLowerCase() || "";
|
||||
const nameLc = req.name.toLowerCase();
|
||||
const nameWords = nameLc.split(" ");
|
||||
let score = 0;
|
||||
|
||||
// prefix
|
||||
if (nameLc.startsWith(searchLc)) {
|
||||
score += 30;
|
||||
}
|
||||
|
||||
// prefix
|
||||
if (projectNameLc.startsWith(searchLc)) {
|
||||
score += 25;
|
||||
}
|
||||
|
||||
// abbreviation
|
||||
if (searchWords.length === 1) {
|
||||
const nameAbbr = nameWords.map(w => w.slice(0, 1)).filter(w => w.length).join("");
|
||||
if (nameAbbr == searchLc) {
|
||||
score += 30;
|
||||
}
|
||||
if (nameAbbr.startsWith(searchLc)) {
|
||||
score += 25;
|
||||
}
|
||||
}
|
||||
|
||||
// tags
|
||||
const allTags = [...req.tags, ...(req.project?.tags||[])];
|
||||
for (const tag of allTags) {
|
||||
const tagLc = tag.toLowerCase();
|
||||
|
||||
if (tagLc === tag) {
|
||||
score += 30
|
||||
} else if (tagLc.startsWith(searchLc)) {
|
||||
score += 30 - (allTags.length * 3)
|
||||
}
|
||||
}
|
||||
|
||||
// project partial match
|
||||
if (projectNameLc.includes(searchLc)) {
|
||||
score += 15;
|
||||
}
|
||||
|
||||
// description partial match
|
||||
if (req.description.toLocaleLowerCase().includes(searchLc)) {
|
||||
score += 4 * searchLc.length;
|
||||
}
|
||||
|
||||
// letters in order
|
||||
let j = 0;
|
||||
let b = 1;
|
||||
for (let i = 0; i < nameLc.length; i++) {
|
||||
if (nameLc.charAt(i) == searchLc.charAt(j)) {
|
||||
score += b;
|
||||
j += 1;
|
||||
b += 1;
|
||||
if (searchLc.charAt(j) === " ") {
|
||||
j += 1;
|
||||
}
|
||||
} else {
|
||||
b = 1;
|
||||
}
|
||||
}
|
||||
score -= (searchLc.length - j) * 2
|
||||
|
||||
scoreMap.set(req.id, score);
|
||||
}
|
||||
|
||||
filteredRequirements = filteredRequirements
|
||||
.filter(r => scoreMap.get(r.id) > searchLc.length )
|
||||
.sort((a,b) => scoreMap.get(b.id) - scoreMap.get(a.id));
|
||||
} else if (filteredRequirements.length > 3) {
|
||||
filteredRequirements = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function onHotkey(e: KeyboardEvent) {
|
||||
if ($currentModal.name === "closed") {
|
||||
if (e.shiftKey && e.key.toLowerCase() === "r") {
|
||||
show = true;
|
||||
tick().then(() => searchBar.focus());
|
||||
reloadRequirementList();
|
||||
closeModal();
|
||||
} else if (e.key.toLowerCase() === "escape") {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keyup", onHotkey);
|
||||
|
||||
return () => window.removeEventListener("keyup", onHotkey);
|
||||
})
|
||||
|
||||
async function submit() {}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<Modal wide nobody show={show} verb="Jump" noun={"to Requrirement"} disabled={loading} error={error}>
|
||||
<ModalBody>
|
||||
<input bind:value={searchRaw} bind:this={searchBar} placeholder="type to search requirements" />
|
||||
<div class="requirement-list">
|
||||
{#each filteredRequirements as req (req.id)}
|
||||
<RequirementSubSection requirement={req} />
|
||||
{/each}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
|
||||
<style lang="sass">
|
||||
@import "../css/colors"
|
||||
|
||||
input
|
||||
font-size: 1.5em
|
||||
background-color: $color-entry1 !important
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
@@ -12,6 +13,7 @@
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {updateScope} = getScopeContext();
|
||||
const {reloadScopeList} = getScopeListContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let scope: ScopeInput
|
||||
let scopeId: number
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
@@ -8,20 +9,23 @@
|
||||
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import PartInput from "$lib/components/controls/PartInput.svelte";
|
||||
import SprintKindSelect from "$lib/components/controls/SprintKindSelect.svelte";
|
||||
import TagInput from "$lib/components/controls/TagInput.svelte";
|
||||
import TimeRangeInput from "$lib/components/controls/TimeRangeInput.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
import { SprintKind, type SprintInput, type SprintInputPart } from "$lib/models/sprint";
|
||||
import { formatFormTime } from "$lib/utils/date";
|
||||
import { partsDiff } from "$lib/utils/sprint";
|
||||
import { formatFormTime } from "$lib/utils/date";
|
||||
import { partsDiff } from "$lib/utils/sprint";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {scope} = getScopeContext();
|
||||
const {reloadSprintList} = getSprintListContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let sprint: SprintInput
|
||||
let sprintId: number
|
||||
let scopeId: number
|
||||
let oldParts: SprintInputPart[]
|
||||
let intervalName: string;
|
||||
|
||||
@@ -58,8 +62,10 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
isTimed: false,
|
||||
isUnweighted: false,
|
||||
parts: [],
|
||||
tags: [],
|
||||
}
|
||||
intervalName = "this_month";
|
||||
scopeId = scope.id;
|
||||
|
||||
op = "Create"
|
||||
openedDate = new Date();
|
||||
@@ -78,8 +84,10 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
isCoarse: current.isCoarse,
|
||||
isTimed: current.isTimed,
|
||||
isUnweighted: current.isUnweighted,
|
||||
tags: [...(current.tags||[])],
|
||||
};
|
||||
sprintId = current.id;
|
||||
scopeId = current.scopeId;
|
||||
intervalName = "specific_dates";
|
||||
|
||||
if (sprint.kind === SprintKind.Stats) {
|
||||
@@ -107,16 +115,16 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
try {
|
||||
switch (op) {
|
||||
case "Create":
|
||||
await sl3(fetch).createSprint($scope.id, submission);
|
||||
await sl3(fetch).createSprint(scopeId, submission);
|
||||
break;
|
||||
case "Edit":
|
||||
await sl3(fetch).updateSprint($scope.id, sprintId, submission);
|
||||
await sl3(fetch).updateSprint(scopeId, sprintId, submission);
|
||||
const {added, removed} = partsDiff(oldParts, submission.parts)
|
||||
for (const part of added) {
|
||||
await sl3(fetch).upsertSprintPart($scope.id, sprintId, part).catch(() => {});
|
||||
await sl3(fetch).upsertSprintPart(scopeId, sprintId, part).catch(() => {});
|
||||
}
|
||||
for (const part of removed) {
|
||||
await sl3(fetch).deleteSprintPart($scope.id, sprintId, part).catch(() => {});
|
||||
await sl3(fetch).deleteSprintPart(scopeId, sprintId, part).catch(() => {});
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -149,19 +157,21 @@ import { partsDiff } from "$lib/utils/sprint";
|
||||
<TimeRangeInput openDate={openedDate} bind:from={sprint.fromTime} bind:to={sprint.toTime} bind:intervalName={intervalName} />
|
||||
<label for="aggregateName">Aggregate Name</label>
|
||||
<input name="aggregateName" type="text" bind:value={sprint.aggregateName} />
|
||||
<label for="tags">Tags</label>
|
||||
<TagInput exclaimMode bind:value={sprint.tags} />
|
||||
{#if sprint.kind != SprintKind.Items}
|
||||
<label for="aggregateValue">Aggregate Goal</label>
|
||||
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} />
|
||||
{/if}
|
||||
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
|
||||
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
|
||||
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
|
||||
</ModalBody>
|
||||
<ModalBody>
|
||||
<label for="kind">Kind</label>
|
||||
<SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} />
|
||||
<label for="parts">Parts</label>
|
||||
<PartInput bind:value={sprint.parts} kind={sprint.kind} />
|
||||
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
|
||||
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
|
||||
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
|
||||
<PartInput bind:value={sprint.parts} kind={sprint.kind} scopeId={scopeId} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</form>
|
||||
@@ -1,19 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getStores } from "$app/stores";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import Modal from "$lib/components/common/Modal.svelte";
|
||||
import ModalBody from "$lib/components/common/ModalBody.svelte";
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
import type { Requirement } from "$lib/models/project";
|
||||
import type Scope from "$lib/models/scope";
|
||||
import type { StatInput } from "$lib/models/stat";
|
||||
import type Stat from "$lib/models/stat";
|
||||
import { getAllContexts } from "svelte";
|
||||
|
||||
const {currentModal, closeModal} = getModalContext();
|
||||
const {scope, upsertStat} = getScopeContext();
|
||||
const {page} = getStores();
|
||||
|
||||
let stat: StatInput
|
||||
let statId: number
|
||||
@@ -103,7 +104,7 @@ import Checkbox from "$lib/components/layout/Checkbox.svelte";
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<Modal closable show={show} verb={op} noun="Item" disabled={loading} error={error}>
|
||||
<Modal closable show={show} verb={op} noun="Stat" disabled={loading} error={error}>
|
||||
<ModalBody>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={stat.name} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TimeInterval } from "./common";
|
||||
import type { StatValueInput, StatProgress } from "./stat";
|
||||
import type Status from "./status";
|
||||
|
||||
export default interface Item {
|
||||
id: number
|
||||
@@ -13,6 +14,26 @@ export default interface Item {
|
||||
acquiredTime?: string
|
||||
scheduledDate?: string
|
||||
stats: StatProgress[]
|
||||
weightedAcquired: number
|
||||
weightedRequired: number
|
||||
project: ItemProject
|
||||
requirement: ItemRequirement
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
interface ItemProject {
|
||||
id: number
|
||||
name: string
|
||||
status: Status
|
||||
statusName: string
|
||||
createdTime: string
|
||||
}
|
||||
|
||||
interface ItemRequirement {
|
||||
id: number
|
||||
name: string
|
||||
status: Status
|
||||
statusName: string
|
||||
}
|
||||
|
||||
export interface ItemInput {
|
||||
@@ -22,7 +43,11 @@ export interface ItemInput {
|
||||
acquiredTime?: string
|
||||
scheduledDate?: string
|
||||
clearAcquiredTime?: boolean
|
||||
clearScheduledDate?: boolean
|
||||
stats: StatValueInput[]
|
||||
tags: string[]
|
||||
addTags?: string[]
|
||||
removeTags?: string[]
|
||||
}
|
||||
|
||||
export interface ItemFilter {
|
||||
@@ -36,6 +61,8 @@ export interface ItemFilter {
|
||||
createdTime?: TimeInterval<Date | string>
|
||||
acquiredTime?: TimeInterval<Date | string>
|
||||
loose?: boolean
|
||||
unAcquired?: boolean
|
||||
unScheduled?: boolean
|
||||
}
|
||||
|
||||
export interface ItemGroup {
|
||||
@@ -46,9 +73,17 @@ export interface ItemGroup {
|
||||
export interface ItemGroupReference {
|
||||
idx: number
|
||||
event: "created" | "acquired" | "scheduled"
|
||||
time: string
|
||||
|
||||
sortKey?: string
|
||||
}
|
||||
|
||||
export interface GroupItemsOptions {
|
||||
hideCreated?: boolean
|
||||
hideAcquired?: boolean
|
||||
hideScheduled?: boolean
|
||||
}
|
||||
|
||||
// this_week
|
||||
// this_month
|
||||
// all_time
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ProjectEntry {
|
||||
name: string
|
||||
status: number
|
||||
statusName: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface ProjectInput {
|
||||
@@ -26,9 +27,12 @@ export interface ProjectInput {
|
||||
name: string
|
||||
description: string
|
||||
status: number
|
||||
tags: string[]
|
||||
addTags?: string[]
|
||||
removeTags?: string[]
|
||||
}
|
||||
|
||||
export interface Requirement {
|
||||
export interface RequirementWithoutItems {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
@@ -37,13 +41,33 @@ export interface Requirement {
|
||||
totalAcquired: number
|
||||
totalRequired: number
|
||||
totalPlanned: number
|
||||
isCoarse: boolean
|
||||
aggregateRequired: number
|
||||
stats: StatProgressWithPlanned[]
|
||||
tags: string[]
|
||||
project?: RequirementProjectLink
|
||||
}
|
||||
|
||||
export interface Requirement extends RequirementWithoutItems {
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export interface RequirementProjectLink {
|
||||
id: number
|
||||
scopeId: number
|
||||
name: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface RequirementInput {
|
||||
name: string
|
||||
description: string
|
||||
status: Status
|
||||
isCoarse: boolean
|
||||
aggregateRequired: number
|
||||
stats: StatValueInput[]
|
||||
tags: string[]
|
||||
addTags?: string[]
|
||||
removeTags?: string[]
|
||||
projectId?: number
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export default interface Sprint {
|
||||
description: string
|
||||
kind: SprintKind
|
||||
|
||||
tags?: string[]
|
||||
|
||||
fromTime: string
|
||||
toTime: string
|
||||
|
||||
@@ -19,15 +21,22 @@ export default interface Sprint {
|
||||
aggregateRequired: number
|
||||
aggregateAcquired: number
|
||||
aggregatePlanned: number
|
||||
aggregateBurndown?: SprintBurndownDataPoint[]
|
||||
|
||||
itemsAcquired?: number
|
||||
itemsRequired?: number
|
||||
itemBurndown?: SprintBurndownDataPoint[]
|
||||
|
||||
partIds: number[]
|
||||
items: Item[]
|
||||
progress: StatProgress[]
|
||||
}
|
||||
|
||||
export interface SprintBurndownDataPoint {
|
||||
date: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export enum SprintKind {
|
||||
Items = 0,
|
||||
Requirements = 1,
|
||||
@@ -49,6 +58,7 @@ export interface SprintInput {
|
||||
aggregateName?: string
|
||||
aggregateRequired?: number
|
||||
parts?: SprintInputPart[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface SprintInputPart {
|
||||
|
||||
@@ -8,4 +8,14 @@ enum Status {
|
||||
Dropped,
|
||||
};
|
||||
|
||||
export const STATUS_NAMES: Record<Status, string> = {
|
||||
[Status.Blocked]: "Blocked",
|
||||
[Status.Available]: "Available",
|
||||
[Status.Background]: "Background",
|
||||
[Status.Active]: "Active",
|
||||
[Status.Completed]: "Completed",
|
||||
[Status.Failed]: "Failed",
|
||||
[Status.Dropped]: "Dropped",
|
||||
}
|
||||
|
||||
export default Status;
|
||||
@@ -1,5 +1,5 @@
|
||||
const weekDay = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
|
||||
const monthNames = ["January","February","March","April","June","july","August","September","October","November","December"]
|
||||
const monthNames = ["January","February","March","April","May","June","july","August","September","October","November","December"]
|
||||
|
||||
export function formatFormTime(time: Date | string): string {
|
||||
if (!time) {
|
||||
@@ -51,8 +51,25 @@ export function formatWeekdayDate(time: Date | string | number): string {
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())} (${weekDay[time.getDay()]})`;
|
||||
}
|
||||
|
||||
export function startOfMonth(now: Date): Date {
|
||||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
export function formatWeekdayTitle(now: Date, time: Date | string | number): string {
|
||||
if (!(time instanceof Date)) {
|
||||
time = new Date(time);
|
||||
}
|
||||
|
||||
const diff = Math.floor((time.getTime() - now.getTime()) / (86400000));
|
||||
if (diff === 0) {
|
||||
return "Today"
|
||||
} else if (diff === -1) {
|
||||
return "Yesterday"
|
||||
} else if (diff >= -6) {
|
||||
return `${weekDay[time.getDay()]}`;
|
||||
} else {
|
||||
return formatWeekdayDate(time);
|
||||
}
|
||||
}
|
||||
|
||||
export function startOfMonth(now: Date, offset = 0): Date {
|
||||
return new Date(now.getFullYear(), now.getMonth() + offset);
|
||||
}
|
||||
|
||||
export function startOfYear(now: Date): Date {
|
||||
@@ -133,4 +150,48 @@ export function addYears(date: Date, years: number): Date {
|
||||
const newDate = new Date(date.getTime());
|
||||
newDate.setFullYear(newDate.getFullYear() + years);
|
||||
return newDate;
|
||||
}
|
||||
|
||||
export function formatDays(date: Date, now: Date) {
|
||||
const d = startOfDay(date);
|
||||
const n = startOfDay(now);
|
||||
const diff = Math.round((d.getTime() - n.getTime()) / (86400000));
|
||||
|
||||
if (diff === 0) {
|
||||
return "today";
|
||||
} else if (diff < 7) {
|
||||
return `${diff}d`;
|
||||
} else {
|
||||
return `${Math.floor(diff / 7)}w${Math.floor(diff % 7)}d`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPrettyTime(date: Date, now: Date) {
|
||||
const d = startOfDay(date);
|
||||
const n = startOfDay(now);
|
||||
|
||||
const diff = Math.round((d.getTime() - n.getTime()) / (86400000));
|
||||
const hhmm = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
|
||||
const dayOfMonth = date.getDate();
|
||||
var ordinalDay = dayOfMonth + "th";
|
||||
if (dayOfMonth == 1) {
|
||||
ordinalDay = dayOfMonth + "st";
|
||||
} else if (dayOfMonth == 2) {
|
||||
ordinalDay = dayOfMonth + "nd"
|
||||
} else if (dayOfMonth == 3) {
|
||||
ordinalDay = dayOfMonth + "rd"
|
||||
}
|
||||
|
||||
if (diff === 0) {
|
||||
return `today at ${hhmm}`;
|
||||
} else if (diff === 1) {
|
||||
return `tomorrow at ${hhmm}`;
|
||||
} else if (diff === -1) {
|
||||
return `yesterday at ${hhmm}`;
|
||||
} else if (diff > -7 && diff < 7) {
|
||||
return `${weekDay[date.getDay()]}, the ${ordinalDay} at ${hhmm}`;
|
||||
} else {
|
||||
return `${ordinalDay} of ${monthName(date)} at ${hhmm}`;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
|
||||
import type Item from "$lib/models/item";
|
||||
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
|
||||
|
||||
export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
|
||||
let groups: Record<string, ItemGroup> = {};
|
||||
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
const createKey = formatDate(item.createdTime)
|
||||
const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
|
||||
|
||||
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
|
||||
addItem(groups, createKey, i, createSortKey, item, "created");
|
||||
}
|
||||
|
||||
if (item.scheduledDate) {
|
||||
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
|
||||
}
|
||||
|
||||
if (item.acquiredTime) {
|
||||
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(groups).sort((a,b) => (
|
||||
b.localeCompare(a)
|
||||
)).map(k => ({
|
||||
...groups[k],
|
||||
list: groups[k].list.sort((a, b) => {
|
||||
return b.sortKey.localeCompare(a.sortKey);
|
||||
}).map(r => ({...r, sortKey: void(0)})),
|
||||
})).filter(g => minDate == null || g.label > minDate);
|
||||
}
|
||||
|
||||
function addItem(groups: Record<string, ItemGroup>, key: string, index: number, sortKey: string, item: Item, event: ItemGroupReference["event"]) {
|
||||
if (groups[key] == null) {
|
||||
groups[key] = {
|
||||
label: formatWeekdayDate(sortKey),
|
||||
list: [],
|
||||
}
|
||||
}
|
||||
|
||||
const i = groups[key].list.findIndex(r => r.idx === index)
|
||||
if (i == -1) {
|
||||
groups[key].list.push({
|
||||
idx: index,
|
||||
event: event,
|
||||
sortKey: sortKey,
|
||||
})
|
||||
} else {
|
||||
groups[key].list[i].event = event;
|
||||
groups[key].list[i].sortKey = sortKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export default function shortestDecimal(n: number) {
|
||||
let s = n.toFixed(3);
|
||||
while (s.endsWith("0")) {
|
||||
s = s.slice(0, -1)
|
||||
}
|
||||
if (s.endsWith(".")) {
|
||||
s = s.slice(0, -1)
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export function significantDecimals(n: number): string {
|
||||
if (n >= 99.95) {
|
||||
return n.toFixed();
|
||||
} else if (n >= 9.95) {
|
||||
return n.toFixed(1);
|
||||
} else if (n >= 0.01) {
|
||||
return n.toFixed(2);
|
||||
} else if (n >= 0.001) {
|
||||
return n.toFixed(3);
|
||||
} else if (n >= 0) {
|
||||
return n.toFixed(2);
|
||||
} else {
|
||||
return "-" + significantDecimals(-n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export default function deepEqual(a: Object, b: Object) {
|
||||
if (a === void(0)) {
|
||||
if (b !== void(0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (a === null) {
|
||||
if (b !== null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Date) {
|
||||
if (b instanceof Date) {
|
||||
b = b.toISOString();
|
||||
}
|
||||
a = a.toISOString();
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b) || b.length !== a.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof(a) === "object") {
|
||||
if (typeof(b) !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function statDiff(before: StatValueInput[], after: StatValueInput[], dele
|
||||
|
||||
for (const s of after) {
|
||||
const old = before.find(s2 => s2.statId === s.statId);
|
||||
if (old == null || old.required != s.required || old.acquired != s.required) {
|
||||
if (old == null || old.required != s.required || old.acquired != s.acquired) {
|
||||
res.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { TimeInterval } from "../models/common";
|
||||
import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date";
|
||||
import { addDays, addMonths, addYears, endOfYear, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date";
|
||||
|
||||
export default function parseInterval(s: string, date: Date): TimeInterval<Date> {
|
||||
const [verb, args] = s.split(":")
|
||||
const [verb, args] = s.split(";")[0].split(":")
|
||||
|
||||
switch (verb) {
|
||||
case "":
|
||||
@@ -25,9 +25,9 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
|
||||
const [amount, unit] = unitNumber(args);
|
||||
switch (unit) {
|
||||
case "d": case "days":
|
||||
return {display: "days", min: startOfDay(addDays(date, -amount)), max: startOfDay(addDays(date, 1))}
|
||||
return {display: "days", min: startOfDay(addDays(date, -(amount - 1))), max: startOfDay(addDays(date, 1))}
|
||||
case "w": case "weeks":
|
||||
return {display: "weeks", min: startOfDay(addDays(date, -amount * 7)), max: startOfDay(addDays(date, 1))}
|
||||
return {display: "weeks", min: startOfDay(addDays(date, -((amount * 7) - 1))), max: startOfDay(addDays(date, 1))}
|
||||
case "m": case "months":
|
||||
return {display: "days", min: startOfDay(addMonths(date, -amount)), max: startOfDay(addDays(date, 1))}
|
||||
case "y": case "years":
|
||||
@@ -43,11 +43,11 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
|
||||
case "last_week":
|
||||
return {display: "weeks", min: startOfWeek(addDays(date, -7)), max: startOfWeek(date)}
|
||||
case "next_month":
|
||||
return {display: "month", min: startOfMonth(addMonths(date, 1)), max: startOfMonth(addMonths(date, 2))}
|
||||
return {display: "month", min: startOfMonth(date, 1), max: startOfMonth(date, 2)}
|
||||
case "this_month":
|
||||
return {display: "month", min: startOfMonth(date), max: startOfMonth(addMonths(date, 1))}
|
||||
return {display: "month", min: startOfMonth(date, 0), max: startOfMonth(date, 1)}
|
||||
case "last_month":
|
||||
return {display: "month", min: startOfMonth(addMonths(date, -1)), max: startOfMonth(date)}
|
||||
return {display: "month", min: startOfMonth(date, -1), max: startOfMonth(date, 0)}
|
||||
case "next_year":
|
||||
return {display: "year", min: startOfYear(addYears(date, 1)), max: startOfYear(addYears(date, 2))}
|
||||
case "this_year":
|
||||
@@ -55,6 +55,23 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
|
||||
case "last_year":
|
||||
return {display: "year", min: startOfYear(addYears(date, -1)), max: startOfYear(date)}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function morningOf(date: Date) {
|
||||
return new Date(startOfDay(date).getTime() + (3600000 * 5));
|
||||
}
|
||||
|
||||
export function morningInterval(interval: TimeInterval<Date>) {
|
||||
if (!interval) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
min: new Date(interval.min.getTime() + 3600000 * 5),
|
||||
max: new Date(interval.max.getTime() + 3600000 * 5),
|
||||
}
|
||||
}
|
||||
|
||||
export function intervalLabel(s: string): string {
|
||||
@@ -101,6 +118,15 @@ export function datesOf(interval: TimeInterval<string | Date>): TimeInterval<str
|
||||
return {min: formatDate(interval.min), max: formatDate(interval.max)}
|
||||
}
|
||||
|
||||
export function scheduledDatesOf(interval: TimeInterval<string | Date>): TimeInterval<string> {
|
||||
const res = datesOf(interval);
|
||||
if (res != null) {
|
||||
res.max = formatDate(endOfYear(addYears(new Date(interval.max), 2)))
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return `${s.charAt(0).toLocaleUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import type { ProjectEntry } from "$lib/models/project";
|
||||
|
||||
export const load: Load = async({ fetch, params }) => {
|
||||
export const load: Load = async({ fetch, params, stuff }) => {
|
||||
const scopeId = parseInt(params.scope.split("-")[0]);
|
||||
const client = sl3(fetch);
|
||||
|
||||
const scope = await sl3(fetch).findScope(scopeId);
|
||||
const projects = await sl3(fetch).listProjects(scopeId);
|
||||
const [scope, projects] = await Promise.all([
|
||||
client.findScope(scopeId),
|
||||
client.listProjects(scopeId),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: { projects, scope },
|
||||
@@ -26,15 +29,17 @@
|
||||
import ProjectListContext from "$lib/components/contexts/ProjectListContext.svelte";
|
||||
import ScopeMenu from "$lib/components/scope/ScopeMenu.svelte";
|
||||
import { scopePrettyId } from "$lib/utils/prettyIds";
|
||||
import ScopeHeader from "$lib/components/scope/ScopeHeader.svelte";
|
||||
import ScopeHeader from "$lib/components/scope/ScopeHeader.svelte";
|
||||
|
||||
export let scope: Scope;
|
||||
export let projects: ProjectEntry[];
|
||||
|
||||
let hideMobile;
|
||||
let hideMobile: boolean;
|
||||
$: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}`
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{scope.abbreviation}: {$page.stuff.title} – Stufflog 3</title></svelte:head>
|
||||
|
||||
<ScopeContext scope={scope}>
|
||||
<ProjectListContext projects={projects}>
|
||||
<Columns wide>
|
||||
|
||||
@@ -2,42 +2,44 @@
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
|
||||
import { groupItems } from "$lib/utils/items";
|
||||
import parseInterval, { scheduledDatesOf } from "$lib/utils/timeinterval";
|
||||
import type Item from "$lib/models/item";
|
||||
import type { ItemFilter, ItemGroup } from "$lib/models/item";
|
||||
|
||||
export const load: Load = async({ fetch, params }) => {
|
||||
export const load: Load = async({ fetch, params, session, stuff }) => {
|
||||
const scopeId = parseInt(params.scope.split("-")[0]);
|
||||
|
||||
const interval = parseInterval(params.interval, new Date());
|
||||
const filter = {
|
||||
createdTime: interval,
|
||||
acquiredTime: interval,
|
||||
scheduledDate: datesOf(interval),
|
||||
scheduledDate: scheduledDatesOf(interval),
|
||||
};
|
||||
const items = await sl3(fetch).listItems(scopeId, filter);
|
||||
const {items, groups} = await sl3(fetch).listItems(scopeId, filter, true, session.groupOptions);
|
||||
|
||||
return {
|
||||
stuff: { title: "History" },
|
||||
props: {
|
||||
items, filter,
|
||||
items, groups, filter,
|
||||
intervalString: params.interval,
|
||||
groups: groupItems(items, datesOf(interval)?.min),
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { session } from "$app/stores";
|
||||
import { browser } from "$app/env";
|
||||
import Main from "$lib/components/layout/Main.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import HistorySelector from "$lib/components/common/HistorySelector.svelte";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import ItemListContext from "$lib/components/contexts/ItemListContext.svelte";
|
||||
import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte";
|
||||
import ToggleIcon from "$lib/components/history/ToggleIcon.svelte";
|
||||
|
||||
export let intervalString: string
|
||||
export let items: Item[]
|
||||
@@ -51,14 +53,39 @@
|
||||
|
||||
lastHistoryRange.set(target.value);
|
||||
|
||||
goto(`./${target.value}`);
|
||||
goto(`./${target.value}`, {replaceState: true});
|
||||
}
|
||||
|
||||
function triggerReload() {
|
||||
goto(`./${intervalString}`, {replaceState: true});
|
||||
}
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
if ($session.groupOptions == null) {
|
||||
try {
|
||||
$session.groupOptions = JSON.parse(localStorage.getItem("sl3.history.group_options")||"{}");
|
||||
triggerReload();
|
||||
} catch(_) {
|
||||
$session.groupOptions = {};
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem("sl3.history.group_options", JSON.stringify($session.groupOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ItemListContext filter={filter} groups={groups} items={items}>
|
||||
<Main big>
|
||||
<HistorySelector value={intervalString} on:change={changeInterval} />
|
||||
<HistoryGroupList />
|
||||
<HistorySelector value={intervalString} on:change={changeInterval} >
|
||||
{#if $session.groupOptions != null}
|
||||
<ToggleIcon inverse name="lightbulb" bind:value={$session.groupOptions.hideCreated} on:click={triggerReload} />
|
||||
<ToggleIcon inverse name="check_slot" bind:value={$session.groupOptions.hideAcquired} on:click={triggerReload} />
|
||||
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
|
||||
{/if}
|
||||
</HistorySelector>
|
||||
<HistoryGroupList intervalString={intervalString} />
|
||||
</Main>
|
||||
|
||||
<ItemCreateModal />
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
|
||||
export const load: Load = async({params, url, fetch}) => {
|
||||
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
|
||||
return {
|
||||
scheduledFilter: {
|
||||
scheduledDate: datesOf(parseInterval("next:30d", now)),
|
||||
unAcquired: true,
|
||||
},
|
||||
acquiredFilter: {
|
||||
acquiredTime: morningInterval(parseInterval("last:15d", now)),
|
||||
},
|
||||
looseFilter: {
|
||||
loose: true,
|
||||
unAcquired: true,
|
||||
unScheduled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const load: Load = async({params, stuff, fetch}) => {
|
||||
const scopeId = parseInt(params.scope.split("-")[0]);
|
||||
|
||||
console.log(
|
||||
datesOf(parseInterval("next:7d", new Date)),
|
||||
parseInterval("today", new Date),
|
||||
)
|
||||
const client = sl3(fetch);
|
||||
|
||||
const scheduledItems = (await sl3(fetch).listItems(scopeId, {
|
||||
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
|
||||
}))
|
||||
const acquiredItems = (await sl3(fetch).listItems(scopeId, {
|
||||
acquiredTime: parseInterval("today", new Date),
|
||||
}))
|
||||
const sprints = await sl3(fetch).listSprints(scopeId);
|
||||
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
|
||||
const [scheduledItems, acquiredItems, looseItems, sprints] = await Promise.all([
|
||||
client.listItems(scopeId, scheduledFilter),
|
||||
client.listItems(scopeId, acquiredFilter),
|
||||
client.listItems(scopeId, looseFilter),
|
||||
client.listSprints(scopeId),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: {scheduledItems, acquiredItems, sprints}
|
||||
stuff: { title: "Overview" },
|
||||
props: {scheduledItems, acquiredItems, looseItems, sprints}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
|
||||
import Column from "$lib/components/layout/Column.svelte";
|
||||
import Columns from "$lib/components/layout/Columns.svelte";
|
||||
@@ -33,10 +49,8 @@
|
||||
import StatSubSection from "$lib/components/scope/StatSubSection.svelte";
|
||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
|
||||
import parseInterval, { datesOf, morningInterval } from "$lib/utils/timeinterval";
|
||||
import type Item from "$lib/models/item";
|
||||
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import Card from "$lib/components/common/Card.svelte";
|
||||
import CardHeader from "$lib/components/common/CardHeader.svelte";
|
||||
@@ -48,13 +62,20 @@
|
||||
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
|
||||
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte";
|
||||
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||
import Icon from "$lib/components/layout/Icon.svelte";
|
||||
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
export let looseItems: Item[];
|
||||
export let sprints: Sprint[];
|
||||
|
||||
const {scope} = getScopeContext();
|
||||
const {openModal} = getModalContext();
|
||||
const {now} = getTimeContext();
|
||||
|
||||
function openCreateProject() {
|
||||
openModal({name: "project.create"});
|
||||
@@ -73,53 +94,44 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<SprintListContext sprints={sprints} intervalString="">
|
||||
<Columns fullwidth>
|
||||
<Column>
|
||||
<Row title="Options">
|
||||
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
|
||||
<Card on:click={openPostItem} pointerCursor><CardHeader>Post Item</CardHeader></Card>
|
||||
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
|
||||
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
|
||||
</Row>
|
||||
{#if scheduledItems.length > 0}
|
||||
<Row title="Schedule">
|
||||
{#each scheduledItems as item (item.id)}
|
||||
<ItemSubSection item={item} />
|
||||
<ItemMultiListContext lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
||||
<SprintListContext sprints={sprints} intervalString="">
|
||||
<Columns fullwidth>
|
||||
<Column>
|
||||
<Row title="Options">
|
||||
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
|
||||
<Card on:click={openPostItem} pointerCursor><CardHeader>Create Item</CardHeader></Card>
|
||||
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
|
||||
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
|
||||
</Row>
|
||||
<ItemListRow event="scheduled" title="Scheduled" key="scheduledItems" />
|
||||
<ItemListRow title="" key="acquiredItems" useDayFilter={15} showAcquiredTime useAcquiredTotal />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</Column>
|
||||
<Column>
|
||||
<Row title="Sprints">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "sprint.create"}}><Icon name="plus" /></Option>
|
||||
</OptionsRow>
|
||||
<SprintList sub />
|
||||
</Row>
|
||||
<Row title="Stats">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "stat.create"}}><Icon name="plus" /></Option>
|
||||
</OptionsRow>
|
||||
{#each $scope.stats as stat (stat.id)}
|
||||
<StatSubSection stat={stat} />
|
||||
{/each}
|
||||
</Row>
|
||||
{/if}
|
||||
{#if acquiredItems.length > 0}
|
||||
<Row title="Today">
|
||||
{#each acquiredItems as item (item.id)}
|
||||
<ItemSubSection item={item} />
|
||||
{/each}
|
||||
</Row>
|
||||
{/if}
|
||||
</Column>
|
||||
<Column>
|
||||
<Row title="Sprints">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "sprint.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
<SprintList sub />
|
||||
</Row>
|
||||
<Row title="Stats">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "stat.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
{#each $scope.stats as stat (stat.id)}
|
||||
<StatSubSection stat={stat} />
|
||||
{/each}
|
||||
</Row>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<StatCreateEditModal />
|
||||
<DeletionModal />
|
||||
<SprintCreateUpdateModal />
|
||||
<ProjectCreateEditModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
</SprintListContext>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<StatCreateEditModal />
|
||||
<DeletionModal />
|
||||
<SprintCreateUpdateModal />
|
||||
<ProjectCreateEditModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
</SprintListContext>
|
||||
</ItemMultiListContext>
|
||||
@@ -10,6 +10,7 @@
|
||||
const project = await sl3(fetch).findProject(scopeId, projectId);
|
||||
|
||||
return {
|
||||
stuff: { title: project.name },
|
||||
props: {project}
|
||||
};
|
||||
}
|
||||
@@ -23,6 +24,7 @@
|
||||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
|
||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte";
|
||||
import MultiScheduleModal from "$lib/modals/MultiScheduleModal.svelte";
|
||||
|
||||
export let project: Project;
|
||||
</script>
|
||||
@@ -34,4 +36,5 @@
|
||||
<RequirementCreateModal />
|
||||
<DeletionModal />
|
||||
<ProjectCreateEditModal />
|
||||
<MultiScheduleModal />
|
||||
</ProjectContext>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
export const load: Load = async({ fetch, params }) => {
|
||||
export const load: Load = async({ fetch, params, stuff }) => {
|
||||
const scopeId = parseInt(params.scope.split("-")[0]);
|
||||
|
||||
let interval = parseInterval(params.interval, new Date());
|
||||
@@ -14,7 +14,10 @@
|
||||
const sprints = await sl3(fetch).listSprints(scopeId, interval);
|
||||
|
||||
return {
|
||||
props: {
|
||||
stuff: {
|
||||
title: "Sprints",
|
||||
},
|
||||
props: {
|
||||
sprints,
|
||||
intervalString: params.interval,
|
||||
},
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
import { Auth, Amplify } from "aws-amplify";
|
||||
import backgroundImage from "$lib/assets/background.jpg";
|
||||
|
||||
export const load: Load = async({ fetch, session, url }) => {
|
||||
if (session.user == null && !url.pathname.startsWith("/login")) {
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
region: import.meta.env.VITE_STUFFLOG3_AWS_POOL_REGION,
|
||||
userPoolId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_ID,
|
||||
userPoolWebClientId: import.meta.env.VITE_STUFFLOG3_AWS_POOL_PUBLIC_CLIENT_ID,
|
||||
},
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const load: Load = async({ url }) => {
|
||||
let idToken: string | null = null;
|
||||
|
||||
try {
|
||||
const sess = await Auth.currentSession();
|
||||
idToken = sess?.getIdToken()?.getJwtToken();
|
||||
} catch(err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
if (idToken == null && !url.pathname.startsWith("/login")) {
|
||||
return { status: 302, redirect: "/login" };
|
||||
}
|
||||
|
||||
return { props: {} };
|
||||
return { props: {}, stuff: { idToken } };
|
||||
}
|
||||
|
||||
export const ssr = false;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -16,15 +38,20 @@
|
||||
import Background from "$lib/components/layout/Background.svelte"
|
||||
import TimeContext from "$lib/components/contexts/TimeContext.svelte";
|
||||
import ModalContext from "$lib/components/contexts/ModalContext.svelte";
|
||||
import RequirementListContext from "$lib/components/contexts/RequirementListContext.svelte";
|
||||
|
||||
let opacity = 0.175;
|
||||
|
||||
$: opacity = $page.url.pathname === "/" ? 0.3 : 0.2;
|
||||
|
||||
export const ssr = false;
|
||||
</script>
|
||||
|
||||
<ModalContext>
|
||||
<TimeContext>
|
||||
<Background initialOrientation="landscape" opacity={opacity} />
|
||||
<slot></slot>
|
||||
<RequirementListContext>
|
||||
<Background src={backgroundImage} initialOrientation="landscape" opacity={opacity} />
|
||||
<slot></slot>
|
||||
</RequirementListContext>
|
||||
</TimeContext>
|
||||
</ModalContext>
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { RequestHandler } from "@sveltejs/kit";
|
||||
|
||||
export const get: RequestHandler = async({ request, url, params, locals }) => {
|
||||
const proxyUrl = `${process.env.STUFFLOG3_API}/api/${params.any}${url.search}`;
|
||||
const proxyUrl = `${import.meta.env.VITE_STUFFLOG3_API}/api/${params.any}${url.search}`;
|
||||
|
||||
const headers = {};
|
||||
if (locals.idToken != null) {
|
||||
headers["Authorization"] = `Bearer ${locals.idToken}`;
|
||||
}
|
||||
request.headers.forEach((v, k) => {
|
||||
if (k.toLowerCase() == "host") {
|
||||
return;
|
||||
}
|
||||
|
||||
headers[k] = v;
|
||||
})
|
||||
|
||||
const res = await fetch(proxyUrl, {
|
||||
method: request.method,
|
||||
@@ -17,15 +21,12 @@ export const get: RequestHandler = async({ request, url, params, locals }) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const body = JSON.stringify(await res.json());
|
||||
const result = {
|
||||
status: res.status,
|
||||
body: await res.text(),
|
||||
headers: {},
|
||||
body,
|
||||
}
|
||||
res.headers.forEach((v, k) => {
|
||||
result.headers[k] = v;
|
||||
})
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,39 @@
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
export const load: Load = async({ fetch, session }) => {
|
||||
const scopes = await sl3(fetch).listScopes();
|
||||
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
|
||||
return {
|
||||
scheduledFilter: {
|
||||
scheduledDate: datesOf(parseInterval("next:30d", now)),
|
||||
unAcquired: true,
|
||||
},
|
||||
acquiredFilter: {
|
||||
acquiredTime: morningInterval(parseInterval("last:15d", now)),
|
||||
},
|
||||
looseFilter: {
|
||||
loose: true,
|
||||
unAcquired: true,
|
||||
unScheduled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const load: Load = async({ fetch }) => {
|
||||
const client = sl3(fetch);
|
||||
|
||||
const scopes = await client.listScopes();
|
||||
|
||||
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
|
||||
|
||||
const [scheduledItems, acquiredItems, looseItems, sprints] = await Promise.all([
|
||||
client.listItems("ALL", scheduledFilter),
|
||||
client.listItems("ALL", acquiredFilter),
|
||||
client.listItems("ALL", looseFilter),
|
||||
client.listSprints("ALL"),
|
||||
]);
|
||||
|
||||
return {
|
||||
props: { scopes }
|
||||
props: { scopes, scheduledItems, acquiredItems, looseItems, sprints }
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -21,26 +49,68 @@
|
||||
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
|
||||
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
|
||||
import Option from "$lib/components/layout/Option.svelte";
|
||||
import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte";
|
||||
import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte";
|
||||
import parseInterval, { datesOf, morningInterval } from "$lib/utils/timeinterval";
|
||||
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
|
||||
import type Item from "$lib/models/item";
|
||||
import DeletionModal from "$lib/modals/DeletionModal.svelte";
|
||||
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
|
||||
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
|
||||
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
|
||||
import type { ItemFilter } from "$lib/models/item";
|
||||
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
|
||||
import SprintList from "$lib/components/scope/SprintList.svelte";
|
||||
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte";
|
||||
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte";
|
||||
import type Sprint from "$lib/models/sprint";
|
||||
import Icon from "$lib/components/layout/Icon.svelte";
|
||||
import RequirementJumpModal from "$lib/modals/RequirementJumpModal.svelte";
|
||||
import RequirementCreateModal from "$lib/modals/RequirementCreateModal.svelte";
|
||||
|
||||
export let scopes: Scope[] = [];
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
export let looseItems: Item[];
|
||||
export let sprints: Sprint[];
|
||||
|
||||
const {now} = getTimeContext();
|
||||
</script>
|
||||
|
||||
<Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header>
|
||||
|
||||
<ScopeListContext scopes={scopes}>
|
||||
<Columns wide>
|
||||
<Column>
|
||||
<Row title="Scopes">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "scope.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
<ScopeLinkList />
|
||||
</Row>
|
||||
</Column>
|
||||
<Column>
|
||||
</Column>
|
||||
</Columns>
|
||||
<svelte:head>
|
||||
<title>Stufflog 3</title>
|
||||
</svelte:head>
|
||||
|
||||
<ScopeCreateUpdateModal />
|
||||
</ScopeListContext>
|
||||
<ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
|
||||
<ScopeListContext scopes={scopes}>
|
||||
<SprintListContext global sprints={sprints} intervalString="">
|
||||
<Columns wide>
|
||||
<Column>
|
||||
<Row title="Scopes">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "scope.create"}}><Icon name="plus" /></Option>
|
||||
</OptionsRow>
|
||||
<ScopeLinkList />
|
||||
</Row>
|
||||
<ItemListRow event="scheduled" title="Scheduled" key="scheduledItems" />
|
||||
<ItemListRow title="" key="acquiredItems" useDayFilter={15} showAcquiredTime useAcquiredTotal />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</Column>
|
||||
<Column>
|
||||
<Row title="Sprints">
|
||||
<SprintList sub />
|
||||
</Row>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<DeletionModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
<SprintCreateUpdateModal />
|
||||
<RequirementCreateModal />
|
||||
<RequirementJumpModal />
|
||||
</SprintListContext>
|
||||
</ScopeListContext>
|
||||
</ItemMultiListContext>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user