mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-07 15:52:47 +01:00
Compare commits
12 Commits
v0.13.0
...
v0.13.7-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f56a2bdb | ||
|
|
88cc02b350 | ||
|
|
46f7815699 | ||
|
|
ea03a3ed23 | ||
|
|
6346140de5 | ||
|
|
d7bdf1d276 | ||
|
|
1e1022ab15 | ||
|
|
a2dd6c05e6 | ||
|
|
96b7a9604c | ||
|
|
21f1c3d328 | ||
|
|
84ae933cfb | ||
|
|
b929100d30 |
11
.github/codecov.yml
vendored
Normal file
11
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
threshold: 5%
|
||||||
|
patch: off
|
||||||
|
comment:
|
||||||
|
layout: diff
|
||||||
|
parsers:
|
||||||
|
go:
|
||||||
|
partials_as_hits: true
|
||||||
7
.github/workflows/goreleaser.yml
vendored
7
.github/workflows/goreleaser.yml
vendored
@@ -7,6 +7,8 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
@@ -21,6 +23,11 @@ jobs:
|
|||||||
|
|
||||||
- run: make build-ui
|
- run: make build-ui
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: "securitybrewery"
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ builds:
|
|||||||
- linux
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
|
|
||||||
|
dockers:
|
||||||
|
- ids: [ catalyst ]
|
||||||
|
dockerfile: docker/goreleaser.Dockerfile
|
||||||
|
image_templates:
|
||||||
|
- "ghcr.io/securitybrewery/catalyst:latest"
|
||||||
|
- "ghcr.io/securitybrewery/catalyst:{{.Tag}}"
|
||||||
|
- "ghcr.io/securitybrewery/catalyst:v{{.Major}}"
|
||||||
|
- "ghcr.io/securitybrewery/catalyst:v{{.Major}}.{{.Minor}}"
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: tar.gz
|
- format: tar.gz
|
||||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||||
|
|||||||
11
Makefile
11
Makefile
@@ -48,6 +48,15 @@ dev:
|
|||||||
go run . fake-data
|
go run . fake-data
|
||||||
go run . serve
|
go run . serve
|
||||||
|
|
||||||
.PHONY: dev-ui
|
.PHONY: dev-10000
|
||||||
|
dev-10000:
|
||||||
|
@echo "Running..."
|
||||||
|
rm -rf catalyst_data
|
||||||
|
go run . admin create admin@catalyst-soar.com 1234567890
|
||||||
|
go run . set-feature-flags dev
|
||||||
|
go run . fake-data --users 100 --tickets 10000
|
||||||
|
go run . serve
|
||||||
|
|
||||||
|
.PHONY: serve-ui
|
||||||
serve-ui:
|
serve-ui:
|
||||||
cd ui && bun dev --port 3000
|
cd ui && bun dev --port 3000
|
||||||
|
|||||||
24
docker/Dockerfile
Normal file
24
docker/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM oven/bun:debian
|
||||||
|
RUN apt-get update && apt-get install -y make
|
||||||
|
COPY .. /tmp/catalyst
|
||||||
|
|
||||||
|
WORKDIR /tmp/catalyst
|
||||||
|
|
||||||
|
RUN make build-ui
|
||||||
|
|
||||||
|
FROM golang:1.23
|
||||||
|
COPY --from=0 /tmp/catalyst /tmp/catalyst
|
||||||
|
|
||||||
|
WORKDIR /tmp/catalyst
|
||||||
|
|
||||||
|
RUN go build -o /usr/local/bin/catalyst
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
COPY --from=1 /usr/local/bin/catalyst /usr/local/bin/catalyst
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
VOLUME /usr/local/bin/catalyst_data
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/catalyst", "serve", "--http", "0.0.0.0:8080"]
|
||||||
9
docker/goreleaser.Dockerfile
Normal file
9
docker/goreleaser.Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
COPY catalyst /usr/local/bin/catalyst
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
VOLUME /usr/local/bin/catalyst_data
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/catalyst", "serve", "--http", "0.0.0.0:8080"]
|
||||||
@@ -128,7 +128,7 @@ func ticketRecords(dao *daos.Dao, users, types []*models.Record, count int) []*m
|
|||||||
record.Set("description", fakeTicketDescription())
|
record.Set("description", fakeTicketDescription())
|
||||||
record.Set("open", gofakeit.Bool())
|
record.Set("open", gofakeit.Bool())
|
||||||
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
||||||
record.Set("state", `{"tlp":"AMBER"}`)
|
record.Set("state", `{"severity":"Medium"}`)
|
||||||
record.Set("owner", random(users).GetId())
|
record.Set("owner", random(users).GetId())
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
@@ -316,7 +316,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record {
|
|||||||
|
|
||||||
record := models.NewRecord(collection)
|
record := models.NewRecord(collection)
|
||||||
record.SetId("w_" + security.PseudorandomString(10))
|
record.SetId("w_" + security.PseudorandomString(10))
|
||||||
record.Set("name", "Test Reaction")
|
record.Set("name", "Alert Ingest Webhook")
|
||||||
record.Set("trigger", "webhook")
|
record.Set("trigger", "webhook")
|
||||||
record.Set("triggerdata", triggerWebhook)
|
record.Set("triggerdata", triggerWebhook)
|
||||||
record.Set("action", "python")
|
record.Set("action", "python")
|
||||||
@@ -334,7 +334,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record {
|
|||||||
|
|
||||||
record = models.NewRecord(collection)
|
record = models.NewRecord(collection)
|
||||||
record.SetId("w_" + security.PseudorandomString(10))
|
record.SetId("w_" + security.PseudorandomString(10))
|
||||||
record.Set("name", "Test Reaction 2")
|
record.Set("name", "Assign new Tickets")
|
||||||
record.Set("trigger", "hook")
|
record.Set("trigger", "hook")
|
||||||
record.Set("triggerdata", triggerHook)
|
record.Set("triggerdata", triggerHook)
|
||||||
record.Set("action", "python")
|
record.Set("action", "python")
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tidwall/sjson v1.2.5
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
go.uber.org/multierr v1.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -229,6 +229,8 @@ go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D
|
|||||||
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
|
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
|
||||||
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
|
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
|
||||||
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
|
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
|
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
|
||||||
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
|
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
@@ -33,7 +35,16 @@ func typeRecords(dao *daos.Dao) []*models.Record {
|
|||||||
record.Set("singular", "Incident")
|
record.Set("singular", "Incident")
|
||||||
record.Set("plural", "Incidents")
|
record.Set("plural", "Incidents")
|
||||||
record.Set("icon", "Flame")
|
record.Set("icon", "Flame")
|
||||||
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
|
record.Set("schema", s(map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"severity": map[string]any{
|
||||||
|
"title": "Severity",
|
||||||
|
"enum": []string{"Low", "Medium", "High"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"severity"},
|
||||||
|
}))
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
|
||||||
@@ -42,9 +53,24 @@ func typeRecords(dao *daos.Dao) []*models.Record {
|
|||||||
record.Set("singular", "Alert")
|
record.Set("singular", "Alert")
|
||||||
record.Set("plural", "Alerts")
|
record.Set("plural", "Alerts")
|
||||||
record.Set("icon", "AlertTriangle")
|
record.Set("icon", "AlertTriangle")
|
||||||
record.Set("schema", `{"type":"object","properties":{"severity":{"title":"Severity","type":"string"}},"required": ["severity"]}`)
|
record.Set("schema", s(map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"severity": map[string]any{
|
||||||
|
"title": "Severity",
|
||||||
|
"enum": []string{"Low", "Medium", "High"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"severity"},
|
||||||
|
}))
|
||||||
|
|
||||||
records = append(records, record)
|
records = append(records, record)
|
||||||
|
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func s(m map[string]any) string {
|
||||||
|
b, _ := json.Marshal(m) //nolint:errchkjson
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|||||||
49
migrations/7_search_view.go
Normal file
49
migrations/7_search_view.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchViewName = "ticket_search"
|
||||||
|
|
||||||
|
const searchViewQuery = `
|
||||||
|
SELECT
|
||||||
|
tickets.id,
|
||||||
|
tickets.name,
|
||||||
|
tickets.created,
|
||||||
|
tickets.description,
|
||||||
|
tickets.open,
|
||||||
|
tickets.type,
|
||||||
|
tickets.state,
|
||||||
|
users.name as owner_name,
|
||||||
|
group_concat(comments.message) as comment_messages,
|
||||||
|
group_concat(files.name) as file_names,
|
||||||
|
group_concat(links.name) as link_names,
|
||||||
|
group_concat(links.url) as link_urls,
|
||||||
|
group_concat(tasks.name) as task_names,
|
||||||
|
group_concat(timeline.message) as timeline_messages
|
||||||
|
FROM tickets
|
||||||
|
LEFT JOIN comments ON comments.ticket = tickets.id
|
||||||
|
LEFT JOIN files ON files.ticket = tickets.id
|
||||||
|
LEFT JOIN links ON links.ticket = tickets.id
|
||||||
|
LEFT JOIN tasks ON tasks.ticket = tickets.id
|
||||||
|
LEFT JOIN timeline ON timeline.ticket = tickets.id
|
||||||
|
LEFT JOIN users ON users.id = tickets.owner
|
||||||
|
GROUP BY tickets.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func searchViewUp(db dbx.Builder) error {
|
||||||
|
return daos.New(db).SaveCollection(internalView(searchViewName, searchViewQuery))
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchViewDown(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
id, err := dao.FindCollectionByNameOrId(searchViewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dao.DeleteCollection(id)
|
||||||
|
}
|
||||||
43
migrations/8_dashboardview.go
Normal file
43
migrations/8_dashboardview.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dashboardCountsViewUpdateQuery = `SELECT id, count FROM (
|
||||||
|
SELECT 'users' as id, COUNT(users.id) as count FROM users
|
||||||
|
UNION
|
||||||
|
SELECT 'tickets' as id, COUNT(tickets.id) as count FROM tickets
|
||||||
|
UNION
|
||||||
|
SELECT 'tasks' as id, COUNT(tasks.id) as count FROM tasks
|
||||||
|
UNION
|
||||||
|
SELECT 'reactions' as id, COUNT(reactions.id) as count FROM reactions
|
||||||
|
) as counts;`
|
||||||
|
|
||||||
|
func dashboardCountsViewUpdateUp(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.Options = types.JsonMap{"query": dashboardCountsViewUpdateQuery}
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dashboardCountsViewUpdateDown(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.Options = types.JsonMap{"query": dashboardCountsViewQuery}
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ func Register() {
|
|||||||
migrations.Register(viewsUp, viewsDown, "1700000004_views.go")
|
migrations.Register(viewsUp, viewsDown, "1700000004_views.go")
|
||||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||||
|
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||||
|
migrations.Register(dashboardCountsViewUpdateUp, dashboardCountsViewUpdateDown, "1700000008_dashboardview.go")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
|
||||||
"github.com/SecurityBrewery/catalyst/migrations"
|
"github.com/SecurityBrewery/catalyst/migrations"
|
||||||
"github.com/SecurityBrewery/catalyst/reaction/action"
|
"github.com/SecurityBrewery/catalyst/reaction/action"
|
||||||
@@ -70,43 +71,49 @@ func runHook(ctx context.Context, app core.App, collection, event string, record
|
|||||||
return fmt.Errorf("failed to marshal webhook payload: %w", err)
|
return fmt.Errorf("failed to marshal webhook payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hook, found, err := findByHookTrigger(app.Dao(), collection, event)
|
hooks, err := findByHookTrigger(app.Dao(), collection, event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to find hook by trigger: %w", err)
|
return fmt.Errorf("failed to find hook by trigger: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if len(hooks) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = action.Run(ctx, app, hook.GetString("action"), hook.GetString("actiondata"), string(payload))
|
var errs error
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run hook reaction: %w", err)
|
for _, hook := range hooks {
|
||||||
|
_, err = action.Run(ctx, app, hook.GetString("action"), hook.GetString("actiondata"), string(payload))
|
||||||
|
if err != nil {
|
||||||
|
errs = multierr.Append(errs, fmt.Errorf("failed to run hook reaction: %w", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func findByHookTrigger(dao *daos.Dao, collection, event string) (*models.Record, bool, error) {
|
func findByHookTrigger(dao *daos.Dao, collection, event string) ([]*models.Record, error) {
|
||||||
records, err := dao.FindRecordsByExpr(migrations.ReactionCollectionName, dbx.HashExp{"trigger": "hook"})
|
records, err := dao.FindRecordsByExpr(migrations.ReactionCollectionName, dbx.HashExp{"trigger": "hook"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("failed to find hook reaction: %w", err)
|
return nil, fmt.Errorf("failed to find hook reaction: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return nil, false, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matchedRecords []*models.Record
|
||||||
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
var hook Hook
|
var hook Hook
|
||||||
if err := json.Unmarshal([]byte(record.GetString("triggerdata")), &hook); err != nil {
|
if err := json.Unmarshal([]byte(record.GetString("triggerdata")), &hook); err != nil {
|
||||||
return nil, false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(hook.Collections, collection) && slices.Contains(hook.Events, event) {
|
if slices.Contains(hook.Collections, collection) && slices.Contains(hook.Events, event) {
|
||||||
return record, true, nil
|
matchedRecords = append(matchedRecords, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false, nil
|
return matchedRecords, nil
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ui/bun.lockb
BIN
ui/bun.lockb
Binary file not shown.
@@ -7,7 +7,7 @@
|
|||||||
<title>Catalyst</title>
|
<title>Catalyst</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="h-screen w-screen"></div>
|
<div id="app" class="h-screen w-screen overflow-hidden"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path ../.gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"easymde": "^2.18.0",
|
"easymde": "^2.18.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
"lucide-vue-next": "^0.365.0",
|
"lucide-vue-next": "^0.365.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@tsconfig/node20": "^20.1.2",
|
"@tsconfig/node20": "^20.1.2",
|
||||||
"@types/lodash.debounce": "^4.0.9",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.11.28",
|
"@types/node": "^20.11.28",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ defineProps<{
|
|||||||
isPending: boolean
|
isPending: boolean
|
||||||
isError: boolean
|
isError: boolean
|
||||||
error: Error | null
|
error: Error | null
|
||||||
value: any
|
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isPending" class="flex justify-center">
|
<div v-if="isPending" class="flex h-full w-full">
|
||||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
<LoaderCircle class="m-auto h-16 w-16 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<Alert v-else-if="isError" variant="destructive" class="mb-4">
|
<Alert v-else-if="isError" variant="destructive" class="mb-4">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{{ error }}</AlertDescription>
|
<AlertDescription>{{ error }}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<slot v-else-if="value" />
|
<slot v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
16
ui/src/components/common/CatalystLogo.vue
Normal file
16
ui/src/components/common/CatalystLogo.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img src="@/assets/flask.svg" alt="Catalyst Logo" :class="cn('dark:hidden', props.class)" />
|
||||||
|
<img
|
||||||
|
src="@/assets/flask_white.svg"
|
||||||
|
alt="Catalyst Logo"
|
||||||
|
:class="cn('hidden dark:flex', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
|
|
||||||
@@ -32,24 +33,31 @@ const {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Card>
|
<Card>
|
||||||
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
No open tasks
|
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
||||||
</div>
|
No open tasks
|
||||||
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
</div>
|
||||||
<span>{{ task.name }}</span>
|
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
||||||
<RouterLink
|
<span>{{ task.name }}</span>
|
||||||
:to="{
|
<RouterLink
|
||||||
name: 'tickets',
|
:to="{
|
||||||
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
name: 'tickets',
|
||||||
}"
|
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
||||||
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
|
}"
|
||||||
>
|
:class="
|
||||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
cn(
|
||||||
Go to {{ task.expand.ticket.name }}
|
buttonVariants({ variant: 'outline', size: 'sm' }),
|
||||||
<ChevronRight class="ml-2 h-4 w-4" />
|
'h-8 w-full sm:ml-auto sm:w-auto'
|
||||||
</span>
|
)
|
||||||
</RouterLink>
|
"
|
||||||
</PanelListElement>
|
>
|
||||||
|
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||||
|
Go to {{ task.expand.ticket.name }}
|
||||||
|
<ChevronRight class="ml-2 h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
</PanelListElement>
|
||||||
|
</TanView>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -42,16 +42,21 @@ const age = (ticket: Ticket) =>
|
|||||||
</div>
|
</div>
|
||||||
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
|
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
|
||||||
<span>{{ ticket.name }}</span>
|
<span>{{ ticket.name }}</span>
|
||||||
<Separator orientation="vertical" class="h-4" />
|
<Separator orientation="vertical" class="hidden h-4 sm:block" />
|
||||||
<span class="text-sm text-muted-foreground">{{ ticket.expand.type.singular }}</span>
|
<span class="text-sm text-muted-foreground">{{ ticket.expand.type.singular }}</span>
|
||||||
<Separator orientation="vertical" class="h-4" />
|
<Separator orientation="vertical" class="hidden h-4 sm:block" />
|
||||||
<span class="text-sm text-muted-foreground">Open since {{ age(ticket) }} days</span>
|
<span class="text-sm text-muted-foreground">Open since {{ age(ticket) }} days</span>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{
|
:to="{
|
||||||
name: 'tickets',
|
name: 'tickets',
|
||||||
params: { type: ticket.type, id: ticket.id }
|
params: { type: ticket.type, id: ticket.id }
|
||||||
}"
|
}"
|
||||||
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
|
:class="
|
||||||
|
cn(
|
||||||
|
buttonVariants({ variant: 'outline', size: 'sm' }),
|
||||||
|
'h-8 w-full sm:ml-auto sm:w-auto'
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||||
Go to {{ ticket.name }}
|
Go to {{ ticket.name }}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ticketsPerWeek = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="tickets">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<LineChart class="h-40" :data="ticketsPerWeek" index="week" :categories="['count']" />
|
<LineChart class="h-40" :data="ticketsPerWeek" index="week" :categories="['count']" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const namedTypes = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="namedTypes">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div v-if="namedTypes" class="flex flex-1 items-center">
|
<div v-if="namedTypes" class="flex flex-1 items-center">
|
||||||
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
|
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { type HTMLAttributes, onMounted, ref } from 'vue'
|
import { type HTMLAttributes, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes['class']
|
class?: HTMLAttributes['class']
|
||||||
defaultValue?: string | number
|
|
||||||
modelValue?: string | number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const modelValue = defineModel<string>({
|
||||||
(e: 'update:modelValue', payload: string | number): void
|
default: ''
|
||||||
}>()
|
|
||||||
|
|
||||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
||||||
passive: true,
|
|
||||||
defaultValue: props.defaultValue
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const textarea = ref<HTMLElement | null>(null)
|
const textarea = ref<HTMLElement | null>(null)
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
|
import isEqual from 'lodash.isequal'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import type { JSONSchema } from '@/lib/types'
|
import type { JSONSchema } from '@/lib/types'
|
||||||
@@ -26,6 +35,11 @@ onMounted(() => {
|
|||||||
watch(
|
watch(
|
||||||
() => formdata.value,
|
() => formdata.value,
|
||||||
() => {
|
() => {
|
||||||
|
const normFormdata = JSON.parse(JSON.stringify(formdata.value))
|
||||||
|
const normModel = JSON.parse(JSON.stringify(model.value))
|
||||||
|
|
||||||
|
if (isEqual(normFormdata, normModel)) return
|
||||||
|
|
||||||
model.value = { ...formdata.value }
|
model.value = { ...formdata.value }
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
@@ -34,6 +48,26 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(property, key) in schema.properties" :key="key">
|
<div v-for="(property, key) in schema.properties" :key="key">
|
||||||
|
<FormField v-if="property.enum" :name="key" v-slot="{ componentField }" v-model="formdata[key]">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel :for="key" class="text-right">
|
||||||
|
{{ property.title }}
|
||||||
|
</FormLabel>
|
||||||
|
<Select :id="key" class="col-span-3" v-bind="componentField">
|
||||||
|
<SelectTrigger class="font-medium">
|
||||||
|
<SelectValue :placeholder="'Select a ' + property.title" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem v-for="option in property.enum" :key="option" :value="option">
|
||||||
|
{{ option }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
v-if="property.type === 'string'"
|
v-if="property.type === 'string'"
|
||||||
:name="key"
|
:name="key"
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const selectedItems = ref<string[]>(props.modelValue)
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => selectedItems.value,
|
() => selectedItems.value,
|
||||||
(value) => emit('update:modelValue', value)
|
(value) => emit('update:modelValue', value),
|
||||||
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import ShortCut from '@/components/ShortCut.vue'
|
|||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// import { Textarea } from '@/components/ui/textarea'
|
|
||||||
// import { Input } from '@/components/ui/input'
|
|
||||||
|
|
||||||
const model = defineModel({
|
const model = defineModel({
|
||||||
type: String
|
type: String
|
||||||
})
|
})
|
||||||
|
|||||||
5
ui/src/components/layout/ColumnBody.vue
Normal file
5
ui/src/components/layout/ColumnBody.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 items-start justify-start overflow-y-auto overflow-x-hidden">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal file
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
small?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('mx-auto flex w-full max-w-[72rem] gap-4 p-4', small && 'max-w-[47rem]')">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
ui/src/components/layout/ColumnHeader.vue
Normal file
25
ui/src/components/layout/ColumnHeader.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title?: string
|
||||||
|
nowrap?: boolean
|
||||||
|
hideSeparator?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('flex min-h-14 flex-wrap items-center gap-2 bg-background p-2', nowrap && 'flex-nowrap')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h1 v-if="title" class="text-xl font-bold">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Separator v-if="!hideSeparator" />
|
||||||
|
</template>
|
||||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
|||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex w-full items-center border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b',
|
'flex w-full flex-col items-start border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b sm:flex-row sm:items-center',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
import IncidentNav from '@/components/sidebar/IncidentNav.vue'
|
import IncidentNav from '@/components/sidebar/IncidentNav.vue'
|
||||||
import NavList from '@/components/sidebar/NavList.vue'
|
import NavList from '@/components/sidebar/NavList.vue'
|
||||||
import UserDropDown from '@/components/sidebar/UserDropDown.vue'
|
import UserDropDown from '@/components/sidebar/UserDropDown.vue'
|
||||||
@@ -7,65 +8,78 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
|
|
||||||
import { Menu } from 'lucide-vue-next'
|
import { Menu } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
import { useCatalystStore } from '@/store/catalyst'
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
const catalystStore = useCatalystStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-[57px] items-center border-b bg-background">
|
<div
|
||||||
<img
|
:class="
|
||||||
src="@/assets/flask.svg"
|
cn(
|
||||||
alt="Catalyst"
|
'flex min-w-48 shrink-0 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
||||||
class="h-8 w-8 dark:hidden"
|
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
||||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
)
|
||||||
/>
|
"
|
||||||
<img
|
|
||||||
src="@/assets/flask_white.svg"
|
|
||||||
alt="Catalyst"
|
|
||||||
class="hidden h-8 w-8 dark:flex"
|
|
||||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
|
||||||
/>
|
|
||||||
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
|
||||||
</div>
|
|
||||||
<NavList
|
|
||||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
|
||||||
:links="[
|
|
||||||
{
|
|
||||||
title: 'Dashboard',
|
|
||||||
icon: 'PanelsTopLeft',
|
|
||||||
variant: 'ghost',
|
|
||||||
to: '/dashboard'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<Separator />
|
|
||||||
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
|
|
||||||
|
|
||||||
<div class="flex-1" />
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
<NavList
|
|
||||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
|
||||||
:links="[
|
|
||||||
{
|
|
||||||
title: 'Reactions',
|
|
||||||
icon: 'Zap',
|
|
||||||
variant: 'ghost',
|
|
||||||
to: '/reactions'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<Separator />
|
|
||||||
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
|
|
||||||
<Separator />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
@click="catalystStore.toggleSidebar()"
|
|
||||||
size="sm"
|
|
||||||
class="m-2 justify-start px-3.5"
|
|
||||||
>
|
>
|
||||||
<Menu class="size-4" />
|
<div class="flex h-[57px] items-center border-b bg-background">
|
||||||
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
|
<CatalystLogo
|
||||||
</Button>
|
class="size-8"
|
||||||
|
:class="{
|
||||||
|
'flex-1': catalystStore.sidebarCollapsed,
|
||||||
|
'mx-3': !catalystStore.sidebarCollapsed
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
||||||
|
</div>
|
||||||
|
<NavList
|
||||||
|
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||||
|
:links="[
|
||||||
|
{
|
||||||
|
title: 'Dashboard',
|
||||||
|
icon: 'PanelsTopLeft',
|
||||||
|
variant: 'ghost',
|
||||||
|
to: '/dashboard'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||||
|
|
||||||
|
<div class="flex-1" />
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
<NavList
|
||||||
|
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||||
|
:links="[
|
||||||
|
{
|
||||||
|
title: 'Reactions',
|
||||||
|
icon: 'Zap',
|
||||||
|
variant: 'ghost',
|
||||||
|
to: '/reactions'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||||
|
<Separator />
|
||||||
|
<div :class="cn('flex h-14 items-center px-3', !catalystStore.sidebarCollapsed && 'px-2')">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
@click="catalystStore.toggleSidebar()"
|
||||||
|
size="default"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-0',
|
||||||
|
catalystStore.sidebarCollapsed && 'w-9',
|
||||||
|
!catalystStore.sidebarCollapsed && 'w-full justify-start px-3'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Menu class="size-4" />
|
||||||
|
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,29 +3,37 @@ import SideBar from '@/components/layout/SideBar.vue'
|
|||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
defineProps<{
|
||||||
|
showDetails?: boolean
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="0">
|
<TooltipProvider :delay-duration="0">
|
||||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||||
|
<SideBar />
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
'w-full flex-initial border-r sm:w-72',
|
||||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
!showDetails && 'flex',
|
||||||
|
showDetails && 'hidden sm:flex'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<SideBar />
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<slot name="list" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-72 flex-initial border-r">
|
<div
|
||||||
<slot name="list" />
|
:class="
|
||||||
</div>
|
cn('flex-1 overflow-hidden', !showDetails && 'hidden sm:flex', showDetails && 'flex')
|
||||||
<div class="flex-1">
|
"
|
||||||
<slot name="single" />
|
>
|
||||||
|
<div class="flex h-full w-full flex-1 flex-col">
|
||||||
|
<slot name="single" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SideBar from '@/components/layout/SideBar.vue'
|
import SideBar from '@/components/layout/SideBar.vue'
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="0">
|
<TooltipProvider :delay-duration="0">
|
||||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||||
<div
|
<SideBar />
|
||||||
:class="
|
<div class="flex h-full w-full flex-col">
|
||||||
cn(
|
<slot />
|
||||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
|
||||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<SideBar />
|
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
|
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
|
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -72,28 +76,29 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reaction">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
<ColumnHeader>
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||||
<div class="ml-auto">
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
<DeleteDialog
|
Back
|
||||||
v-if="reaction"
|
</Button>
|
||||||
collection="reactions"
|
<div class="ml-auto">
|
||||||
:id="reaction.id"
|
<DeleteDialog
|
||||||
:name="reaction.name"
|
v-if="reaction"
|
||||||
:singular="'Reaction'"
|
collection="reactions"
|
||||||
:to="{ name: 'reactions' }"
|
:id="reaction.id"
|
||||||
:queryKey="['reactions']"
|
:name="reaction.name"
|
||||||
/>
|
:singular="'Reaction'"
|
||||||
</div>
|
:to="{ name: 'reactions' }"
|
||||||
|
:queryKey="['reactions']"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
</ColumnHeader>
|
||||||
|
|
||||||
<ScrollArea v-if="reaction" class="flex-1">
|
<ColumnBody v-if="reaction">
|
||||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
<ColumnBodyContainer small>
|
||||||
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
||||||
</div>
|
</ColumnBodyContainer>
|
||||||
</ScrollArea>
|
</ColumnBody>
|
||||||
</div>
|
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ const curlExample = computed(() => {
|
|||||||
let cmd = `curl`
|
let cmd = `curl`
|
||||||
|
|
||||||
if (values.triggerdata.token) {
|
if (values.triggerdata.token) {
|
||||||
cmd += ` -H "Auth: Bearer ${values.triggerdata.token}"`
|
cmd += ` -H "Authorization: Bearer ${values.triggerdata.token}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.triggerdata.path) {
|
if (values.triggerdata.path) {
|
||||||
@@ -239,7 +239,7 @@ const curlExample = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit="onSubmit" class="flex flex-col items-start gap-4">
|
<form @submit="onSubmit" class="flex w-full flex-col items-start gap-4">
|
||||||
<FormField name="name" v-slot="{ componentField }" validate-on-input>
|
<FormField name="name" v-slot="{ componentField }" validate-on-input>
|
||||||
<FormItem class="w-full">
|
<FormItem class="w-full">
|
||||||
<FormLabel for="name" class="text-right">Name</FormLabel>
|
<FormLabel for="name" class="text-right">Name</FormLabel>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
|
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
@@ -63,32 +63,28 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reactions">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div class="flex h-screen flex-col">
|
<ColumnHeader title="Reactions">
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<div class="ml-auto">
|
||||||
<h1 class="text-xl font-bold">Reactions</h1>
|
<Button variant="ghost" @click="openNew">New Reaction</Button>
|
||||||
<div class="ml-auto">
|
|
||||||
<Button variant="ghost" @click="openNew"> New Reaction</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div class="mt-2 flex flex-1 flex-col gap-2 p-4 pt-0">
|
|
||||||
<TransitionGroup name="list" appear>
|
|
||||||
<ResourceListElement
|
|
||||||
v-for="reaction in reactions"
|
|
||||||
:key="reaction.id"
|
|
||||||
:title="reaction.name"
|
|
||||||
:created="reaction.created"
|
|
||||||
:subtitle="subtitle(reaction)"
|
|
||||||
description=""
|
|
||||||
:active="route.params.id === reaction.id"
|
|
||||||
:to="{ name: 'reactions', params: { id: reaction.id } }"
|
|
||||||
:open="false"
|
|
||||||
>
|
|
||||||
{{ reaction.name }}
|
|
||||||
</ResourceListElement>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ColumnHeader>
|
||||||
|
<div class="mt-2 flex flex-1 flex-col gap-2 p-2 pt-0">
|
||||||
|
<TransitionGroup name="list" appear>
|
||||||
|
<ResourceListElement
|
||||||
|
v-for="reaction in reactions"
|
||||||
|
:key="reaction.id"
|
||||||
|
:title="reaction.name"
|
||||||
|
:created="reaction.created"
|
||||||
|
:subtitle="subtitle(reaction)"
|
||||||
|
description=""
|
||||||
|
:active="route.params.id === reaction.id"
|
||||||
|
:to="{ name: 'reactions', params: { id: reaction.id } }"
|
||||||
|
:open="false"
|
||||||
|
>
|
||||||
|
{{ reaction.name }}
|
||||||
|
</ResourceListElement>
|
||||||
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
|
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -24,14 +28,16 @@ const addReactionMutation = useMutation({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
<ColumnHeader>
|
||||||
<div class="flex min-h-14 items-center bg-background px-4 py-2"></div>
|
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||||
<Separator />
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
<ScrollArea class="flex-1">
|
<ColumnBody>
|
||||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
<ColumnBodyContainer small>
|
||||||
<ReactionForm @submit="addReactionMutation.mutate" />
|
<ReactionForm @submit="addReactionMutation.mutate" />
|
||||||
</div>
|
</ColumnBodyContainer>
|
||||||
</ScrollArea>
|
</ColumnBody>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
|
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
|
||||||
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
|
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { Check, CircleDot, Repeat } from 'lucide-vue-next'
|
import { Check, ChevronLeft, CircleDot, Repeat } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
@@ -49,7 +50,7 @@ const changeTypeMutation = useMutation({
|
|||||||
}),
|
}),
|
||||||
onSuccess: (data: Ticket) => {
|
onSuccess: (data: Ticket) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['tickets'] })
|
queryClient.invalidateQueries({ queryKey: ['tickets'] })
|
||||||
router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
|
// router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
|
||||||
},
|
},
|
||||||
onError: handleError
|
onError: handleError
|
||||||
})
|
})
|
||||||
@@ -74,74 +75,81 @@ const closeTicketDialogOpen = ref(false)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between bg-background p-2">
|
<ColumnHeader>
|
||||||
<div class="flex items-center gap-2">
|
<Button
|
||||||
<Tooltip>
|
@click="router.push({ name: 'tickets', params: { type: ticket.type } })"
|
||||||
<TooltipTrigger as-child>
|
variant="outline"
|
||||||
<div>
|
class="sm:hidden"
|
||||||
<DropdownMenu>
|
>
|
||||||
<DropdownMenuTrigger as-child>
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
<Button variant="outline" :disabled="!ticket">
|
Back
|
||||||
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
</Button>
|
||||||
{{ ticket.expand.type.singular }}
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger as-child>
|
||||||
</DropdownMenuTrigger>
|
<div>
|
||||||
<DropdownMenuContent>
|
<DropdownMenu>
|
||||||
<DropdownMenuItem
|
<DropdownMenuTrigger as-child>
|
||||||
v-for="type in otherTypes"
|
<Button variant="outline" :disabled="!ticket">
|
||||||
:key="type.id"
|
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
||||||
class="cursor-pointer"
|
{{ ticket.expand.type.singular }}
|
||||||
@click="changeTypeMutation.mutate(type.id)"
|
</Button>
|
||||||
>
|
</DropdownMenuTrigger>
|
||||||
<Icon :name="type.icon" class="mr-2 size-4" />
|
<DropdownMenuContent>
|
||||||
Convert to {{ type.singular }}
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
v-for="type in otherTypes"
|
||||||
</DropdownMenuContent>
|
:key="type.id"
|
||||||
</DropdownMenu>
|
class="cursor-pointer"
|
||||||
</div>
|
@click="changeTypeMutation.mutate(type.id)"
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>Change Type</TooltipContent>
|
<Icon :name="type.icon" class="mr-2 size-4" />
|
||||||
</Tooltip>
|
Convert to {{ type.singular }}
|
||||||
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
</DropdownMenuItem>
|
||||||
<Tooltip>
|
</DropdownMenuContent>
|
||||||
<TooltipTrigger as-child>
|
</DropdownMenu>
|
||||||
<div>
|
</div>
|
||||||
<DropdownMenu>
|
</TooltipTrigger>
|
||||||
<DropdownMenuTrigger as-child>
|
<TooltipContent>Change Type</TooltipContent>
|
||||||
<Button variant="outline" :disabled="!ticket">
|
</Tooltip>
|
||||||
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
||||||
<Check v-else class="mr-2 h-4 w-4" />
|
<Tooltip>
|
||||||
{{ ticket?.open ? 'Open' : 'Closed' }}
|
<TooltipTrigger as-child>
|
||||||
</Button>
|
<div>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenu>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuTrigger as-child>
|
||||||
<DropdownMenuItem
|
<Button variant="outline" :disabled="!ticket">
|
||||||
v-if="ticket.open"
|
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
||||||
class="cursor-pointer"
|
<Check v-else class="mr-2 h-4 w-4" />
|
||||||
@click="closeTicketDialogOpen = true"
|
{{ ticket?.open ? 'Open' : 'Closed' }}
|
||||||
>
|
</Button>
|
||||||
<Check class="mr-2 size-4" />
|
</DropdownMenuTrigger>
|
||||||
Close Ticket
|
<DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
v-if="ticket.open"
|
||||||
<Repeat class="mr-2 size-4" />
|
class="cursor-pointer"
|
||||||
Reopen Ticket
|
@click="closeTicketDialogOpen = true"
|
||||||
</DropdownMenuItem>
|
>
|
||||||
</DropdownMenuContent>
|
<Check class="mr-2 size-4" />
|
||||||
</DropdownMenu>
|
Close Ticket
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</TooltipTrigger>
|
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
||||||
<TooltipContent>Change Status</TooltipContent>
|
<Repeat class="mr-2 size-4" />
|
||||||
</Tooltip>
|
Reopen Ticket
|
||||||
<Tooltip>
|
</DropdownMenuItem>
|
||||||
<TooltipTrigger as-child>
|
</DropdownMenuContent>
|
||||||
<div>
|
</DropdownMenu>
|
||||||
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>Change Status</TooltipContent>
|
||||||
<TooltipContent>Change User</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip>
|
||||||
</div>
|
<TooltipTrigger as-child>
|
||||||
|
<div>
|
||||||
|
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Change User</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<div class="-mx-1 flex-1" />
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
v-if="ticket"
|
v-if="ticket"
|
||||||
:collection="'tickets'"
|
:collection="'tickets'"
|
||||||
@@ -151,5 +159,5 @@ const closeTicketDialogOpen = ref(false)
|
|||||||
:to="{ name: 'tickets' }"
|
:to="{ name: 'tickets' }"
|
||||||
:queryKey="['tickets']"
|
:queryKey="['tickets']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</ColumnHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between gap-2 bg-background p-2">
|
<ColumnHeader nowrap hideSeparator>
|
||||||
<Input v-if="ticket.open" v-model="resolution" placeholder="Closing reason" />
|
<Input v-if="ticket.open" v-model="resolution" placeholder="Closing reason" />
|
||||||
<div v-else class="flex-1">
|
<div v-else class="flex-1">
|
||||||
<p class="ml-2 text-gray-500">Closed: {{ ticket.resolution }}</p>
|
<p class="ml-2 text-gray-500">Closed: {{ ticket.resolution }}</p>
|
||||||
@@ -56,5 +57,5 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
|
|||||||
: 'Reopen ' + props.ticket.expand.type.singular
|
: 'Reopen ' + props.ticket.expand.type.singular
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ColumnHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
|
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
|
||||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
|
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||||
import StatusIcon from '@/components/ticket/StatusIcon.vue'
|
import StatusIcon from '@/components/ticket/StatusIcon.vue'
|
||||||
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
|
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
|
||||||
import TicketCloseBar from '@/components/ticket/TicketCloseBar.vue'
|
import TicketCloseBar from '@/components/ticket/TicketCloseBar.vue'
|
||||||
@@ -15,14 +17,13 @@ import TicketTimeline from '@/components/ticket/timeline/TicketTimeline.vue'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
import { Edit } from 'lucide-vue-next'
|
import { Edit } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
@@ -100,87 +101,97 @@ const updateDescription = (value: string) => (message.value = value)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="ticket">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div v-if="ticket" class="flex h-full flex-col">
|
<template v-if="ticket">
|
||||||
<TicketActionBar :ticket="ticket" />
|
<TicketActionBar :ticket="ticket" class="shrink-0" />
|
||||||
<Separator />
|
<ColumnBody>
|
||||||
<div class="flex w-full max-w-7xl flex-1 flex-col overflow-hidden xl:m-auto xl:flex-row">
|
<ColumnBodyContainer class="flex-col gap-4 xl:flex-row">
|
||||||
<div class="flex flex-1 flex-col gap-4 px-4 pt-4">
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
<TicketHeader :ticket="ticket" />
|
<TicketHeader :ticket="ticket" />
|
||||||
<Card class="relative p-4">
|
<Card class="relative p-4">
|
||||||
<Button v-if="!editMode" variant="outline" class="float-right h-8 gap-2" @click="edit">
|
<Button
|
||||||
<Edit class="h-3.5 w-3.5" />
|
v-if="!editMode"
|
||||||
<span>Edit</span>
|
variant="outline"
|
||||||
</Button>
|
class="float-right h-8 gap-2"
|
||||||
<DynamicMDEditor
|
@click="edit"
|
||||||
:modelValue="ticket.description"
|
>
|
||||||
@update:modelValue="updateDescription"
|
<Edit class="h-3.5 w-3.5" />
|
||||||
v-model:edit="editMode"
|
<span>Edit</span>
|
||||||
autofocus
|
</Button>
|
||||||
placeholder="Type a description..."
|
<DynamicMDEditor
|
||||||
@save="editDescriptionMutation.mutate"
|
:modelValue="ticket.description"
|
||||||
class="min-h-14"
|
@update:modelValue="updateDescription"
|
||||||
/>
|
v-model:edit="editMode"
|
||||||
</Card>
|
autofocus
|
||||||
<Separator />
|
placeholder="Type a description..."
|
||||||
<Tabs default-value="timeline" class="flex flex-1 flex-col overflow-hidden">
|
@save="editDescriptionMutation.mutate"
|
||||||
<TabsList>
|
class="min-h-14"
|
||||||
<TabsTrigger value="timeline">
|
/>
|
||||||
Timeline
|
</Card>
|
||||||
<Badge
|
<Separator />
|
||||||
v-if="
|
<Tabs default-value="timeline" class="flex flex-1 flex-col">
|
||||||
ticket.expand.timeline_via_ticket &&
|
<TabsList>
|
||||||
ticket.expand.timeline_via_ticket.length > 0
|
<TabsTrigger value="timeline">
|
||||||
"
|
Timeline
|
||||||
variant="outline"
|
<Badge
|
||||||
class="ml-2"
|
v-if="
|
||||||
>
|
ticket.expand.timeline_via_ticket &&
|
||||||
{{
|
ticket.expand.timeline_via_ticket.length > 0
|
||||||
ticket.expand.timeline_via_ticket ? ticket.expand.timeline_via_ticket.length : 0
|
"
|
||||||
}}
|
variant="outline"
|
||||||
</Badge>
|
class="ml-2 hidden sm:inline-flex"
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="tasks">
|
{{
|
||||||
Tasks
|
ticket.expand.timeline_via_ticket
|
||||||
<Badge
|
? ticket.expand.timeline_via_ticket.length
|
||||||
v-if="ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0"
|
: 0
|
||||||
variant="outline"
|
}}
|
||||||
class="ml-2"
|
</Badge>
|
||||||
>
|
</TabsTrigger>
|
||||||
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
<TabsTrigger value="tasks">
|
||||||
<StatusIcon :status="taskStatus" class="size-6" />
|
Tasks
|
||||||
</Badge>
|
<Badge
|
||||||
</TabsTrigger>
|
v-if="
|
||||||
<TabsTrigger value="comments">
|
ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0
|
||||||
Comments
|
"
|
||||||
<Badge
|
variant="outline"
|
||||||
v-if="
|
class="ml-2 hidden sm:inline-flex"
|
||||||
ticket.expand.comments_via_ticket &&
|
>
|
||||||
ticket.expand.comments_via_ticket.length > 0
|
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
||||||
"
|
<StatusIcon :status="taskStatus" class="size-6" />
|
||||||
variant="outline"
|
</Badge>
|
||||||
class="ml-2"
|
</TabsTrigger>
|
||||||
>
|
<TabsTrigger value="comments">
|
||||||
{{
|
Comments
|
||||||
ticket.expand.comments_via_ticket ? ticket.expand.comments_via_ticket.length : 0
|
<Badge
|
||||||
}}
|
v-if="
|
||||||
</Badge>
|
ticket.expand.comments_via_ticket &&
|
||||||
</TabsTrigger>
|
ticket.expand.comments_via_ticket.length > 0
|
||||||
</TabsList>
|
"
|
||||||
<TicketTab value="timeline">
|
variant="outline"
|
||||||
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
class="ml-2 hidden sm:inline-flex"
|
||||||
</TicketTab>
|
>
|
||||||
<TicketTab value="tasks">
|
{{
|
||||||
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
|
ticket.expand.comments_via_ticket
|
||||||
</TicketTab>
|
? ticket.expand.comments_via_ticket.length
|
||||||
<TicketTab value="comments">
|
: 0
|
||||||
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
|
}}
|
||||||
</TicketTab>
|
</Badge>
|
||||||
</Tabs>
|
</TabsTrigger>
|
||||||
<Separator class="xl:hidden" />
|
</TabsList>
|
||||||
</div>
|
<TicketTab value="timeline">
|
||||||
<ScrollArea>
|
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
||||||
<div class="flex flex-initial flex-col gap-4 p-4 xl:w-96">
|
</TicketTab>
|
||||||
|
<TicketTab value="tasks">
|
||||||
|
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
|
||||||
|
</TicketTab>
|
||||||
|
<TicketTab value="comments">
|
||||||
|
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
|
||||||
|
</TicketTab>
|
||||||
|
</Tabs>
|
||||||
|
<Separator class="xl:hidden" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4 xl:w-96 xl:flex-initial">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
|
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
|
||||||
<span class="text-sm font-semibold"> Details </span>
|
<span class="text-sm font-semibold"> Details </span>
|
||||||
@@ -196,10 +207,10 @@ const updateDescription = (value: string) => (message.value = value)
|
|||||||
<Separator />
|
<Separator />
|
||||||
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
|
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ColumnBodyContainer>
|
||||||
</div>
|
</ColumnBody>
|
||||||
<Separator />
|
<Separator />
|
||||||
<TicketCloseBar :ticket="ticket" />
|
<TicketCloseBar :ticket="ticket" class="shrink-0" />
|
||||||
</div>
|
</template>
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ const updateName = (value: string) => {
|
|||||||
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
|
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="flex flex-row space-x-2 px-1 text-xs">
|
<div class="flex flex-col items-stretch gap-1 text-xs text-muted-foreground md:h-4 md:flex-row">
|
||||||
<div class="flex items-center gap-1 text-muted-foreground">
|
<div>
|
||||||
Created:
|
Created:
|
||||||
{{ format(new Date(ticket.created), 'PPpp') }}
|
{{ format(new Date(ticket.created), 'PPpp') }}
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" class="hidden md:block" />
|
||||||
<div class="flex items-center gap-1 text-muted-foreground">
|
<div>
|
||||||
Updated:
|
Updated:
|
||||||
{{ format(new Date(ticket.updated), 'PPpp') }}
|
{{ format(new Date(ticket.updated), 'PPpp') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import TicketListList from '@/components/ticket/TicketListList.vue'
|
import TicketListList from '@/components/ticket/TicketListList.vue'
|
||||||
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
|
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
@@ -14,12 +15,10 @@ import {
|
|||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrev
|
PaginationPrev
|
||||||
} from '@/components/ui/pagination'
|
} from '@/components/ui/pagination'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
import { Info, LoaderCircle, Search } from 'lucide-vue-next'
|
import { LoaderCircle, Search } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
@@ -28,7 +27,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
import type { Ticket, Type } from '@/lib/types'
|
import type { SearchTicket, Type } from '@/lib/types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,15 +47,13 @@ const filter = computed(() => {
|
|||||||
let raws: Array<string> = [
|
let raws: Array<string> = [
|
||||||
'name ~ {:search}',
|
'name ~ {:search}',
|
||||||
'description ~ {:search}',
|
'description ~ {:search}',
|
||||||
'owner.name ~ {:search}',
|
'owner_name ~ {:search}',
|
||||||
'owner.email ~ {:search}',
|
'comment_messages ~ {:search}',
|
||||||
'links_via_ticket.name ~ {:search}',
|
'file_names ~ {:search}',
|
||||||
'links_via_ticket.url ~ {:search}',
|
'link_names ~ {:search}',
|
||||||
'tasks_via_ticket.name ~ {:search}',
|
'link_urls ~ {:search}',
|
||||||
'comments_via_ticket.message ~ {:search}',
|
'task_names ~ {:search}',
|
||||||
'files_via_ticket.name ~ {:search}',
|
'timeline_messages ~ {:search}'
|
||||||
'timeline_via_ticket.message ~ {:search}',
|
|
||||||
'state.severity ~ {:search}'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
||||||
@@ -96,12 +93,10 @@ const {
|
|||||||
refetch
|
refetch
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['tickets', filter.value],
|
queryKey: ['tickets', filter.value],
|
||||||
queryFn: (): Promise<ListResult<Ticket>> =>
|
queryFn: (): Promise<ListResult<SearchTicket>> =>
|
||||||
pb.collection('tickets').getList(page.value, perPage.value, {
|
pb.collection('ticket_search').getList(page.value, perPage.value, {
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
filter: filter.value,
|
filter: filter.value
|
||||||
expand:
|
|
||||||
'type,owner,comments_via_ticket.author,files_via_ticket,timeline_via_ticket,links_via_ticket,tasks_via_ticket.owner'
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,7 +106,7 @@ watch(
|
|||||||
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
|
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'tickets',
|
name: 'tickets',
|
||||||
params: { type: props.selectedType.id, id: ticketItems.value.items[0].id }
|
params: { type: props.selectedType.id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,108 +121,81 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen flex-col">
|
<ColumnHeader :title="selectedType?.plural">
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<div class="ml-auto">
|
||||||
<h1 class="text-xl font-bold">
|
<TicketNewDialog :selectedType="selectedType" />
|
||||||
{{ selectedType?.plural }}
|
</div>
|
||||||
</h1>
|
</ColumnHeader>
|
||||||
<div class="ml-auto">
|
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||||
<TicketNewDialog :selectedType="selectedType" />
|
<div class="flex items-center justify-between px-2 pt-2">
|
||||||
</div>
|
<TabsList>
|
||||||
|
<TabsTrigger value="all">All</TabsTrigger>
|
||||||
|
<TabsTrigger value="open">Open</TabsTrigger>
|
||||||
|
<TabsTrigger value="closed">Closed</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
||||||
|
<ListFilter class="h-3.5 w-3.5" />
|
||||||
|
<span class="sr-only sm:not-sr-only">Filter</span>
|
||||||
|
</Button-->
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<form>
|
||||||
|
<div class="relative flex flex-row items-center">
|
||||||
|
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
|
||||||
|
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||||
|
<Search class="size-4 text-muted-foreground" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
||||||
<div class="flex items-center justify-between px-4 pt-2">
|
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
||||||
<TabsList>
|
</div>
|
||||||
<TabsTrigger value="all"> All</TabsTrigger>
|
<Alert v-else-if="isError" variant="destructive" class="mb-2 h-screen w-screen">
|
||||||
<TabsTrigger value="open"> Open</TabsTrigger>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<TabsTrigger value="closed"> Closed</TabsTrigger>
|
<AlertDescription>{{ error }}</AlertDescription>
|
||||||
</TabsList>
|
</Alert>
|
||||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
<div v-else-if="ticketItems" class="flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
<ListFilter class="h-3.5 w-3.5" />
|
<TicketListList :tickets="ticketItems.items" />
|
||||||
<span class="sr-only sm:not-sr-only">Filter</span>
|
</div>
|
||||||
</Button-->
|
<Separator />
|
||||||
</div>
|
<div class="my-2 flex items-center justify-center">
|
||||||
<div class="px-4 py-2">
|
<span class="text-xs text-muted-foreground">
|
||||||
<form>
|
{{ ticketItems ? ticketItems.items.length : '?' }} of
|
||||||
<div class="relative flex flex-row items-center">
|
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
|
||||||
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
|
</span>
|
||||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
</div>
|
||||||
<Search class="size-4 text-muted-foreground" />
|
<div class="mb-2 flex items-center justify-center">
|
||||||
</span>
|
<Pagination
|
||||||
|
v-slot="{ page }"
|
||||||
|
:total="ticketItems ? ticketItems.totalItems : 0"
|
||||||
|
:itemsPerPage="perPage"
|
||||||
|
:sibling-count="0"
|
||||||
|
:default-page="1"
|
||||||
|
@update:page="page = $event"
|
||||||
|
>
|
||||||
|
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||||
|
<PaginationFirst />
|
||||||
|
<PaginationPrev />
|
||||||
|
|
||||||
<div>
|
<template v-for="(item, index) in items">
|
||||||
<TooltipProvider :delay-duration="0">
|
<PaginationListItem
|
||||||
<Tooltip>
|
v-if="item.type === 'page'"
|
||||||
<TooltipTrigger as-child>
|
:key="index"
|
||||||
<Info class="ml-2 size-4 text-muted-foreground" />
|
:value="item.value"
|
||||||
</TooltipTrigger>
|
as-child
|
||||||
<TooltipContent>
|
>
|
||||||
<p class="w-64">
|
<Button class="h-10 w-10 p-0" :variant="item.value === page ? 'default' : 'outline'">
|
||||||
Search name, description, or owner. Links, tasks, comments, files, and
|
{{ item.value }}
|
||||||
timeline messages are also searched, but cause unreliable results if there are
|
</Button>
|
||||||
more than 1000 records.
|
</PaginationListItem>
|
||||||
</p>
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
</TooltipContent>
|
</template>
|
||||||
</Tooltip>
|
<PaginationNext />
|
||||||
</TooltipProvider>
|
<PaginationLast />
|
||||||
</div>
|
</PaginationList>
|
||||||
</div>
|
</Pagination>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</Tabs>
|
||||||
<Separator />
|
|
||||||
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
|
||||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
<Alert v-else-if="isError" variant="destructive" class="mb-4 h-screen w-screen">
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{{ error }}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
<ScrollArea v-else-if="ticketItems" class="flex-1">
|
|
||||||
<TicketListList :tickets="ticketItems.items" />
|
|
||||||
</ScrollArea>
|
|
||||||
<Separator />
|
|
||||||
<div class="my-2 flex items-center justify-center">
|
|
||||||
<span class="text-xs text-muted-foreground">
|
|
||||||
{{ ticketItems ? ticketItems.items.length : '?' }} of
|
|
||||||
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4 flex items-center justify-center">
|
|
||||||
<Pagination
|
|
||||||
v-slot="{ page }"
|
|
||||||
:total="ticketItems ? ticketItems.totalItems : 0"
|
|
||||||
:itemsPerPage="perPage"
|
|
||||||
:sibling-count="0"
|
|
||||||
:default-page="1"
|
|
||||||
@update:page="page = $event"
|
|
||||||
>
|
|
||||||
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
|
||||||
<PaginationFirst />
|
|
||||||
<PaginationPrev />
|
|
||||||
|
|
||||||
<template v-for="(item, index) in items">
|
|
||||||
<PaginationListItem
|
|
||||||
v-if="item.type === 'page'"
|
|
||||||
:key="index"
|
|
||||||
:value="item.value"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
class="h-10 w-10 p-0"
|
|
||||||
:variant="item.value === page ? 'default' : 'outline'"
|
|
||||||
>
|
|
||||||
{{ item.value }}
|
|
||||||
</Button>
|
|
||||||
</PaginationListItem>
|
|
||||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PaginationNext />
|
|
||||||
<PaginationLast />
|
|
||||||
</PaginationList>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
|
||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import type { Ticket } from '@/lib/types'
|
import type { SearchTicket } from '@/lib/types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tickets: Array<Ticket>
|
tickets: Array<SearchTicket>
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-4 pt-0">
|
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-2 pt-0">
|
||||||
<ResourceListElement
|
<ResourceListElement
|
||||||
v-for="item of tickets"
|
v-for="item of tickets"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:created="item.created"
|
:created="item.created"
|
||||||
:subtitle="item.expand.owner ? item.expand.owner.name : ''"
|
:subtitle="item.owner_name"
|
||||||
:description="item.description ? item.description.substring(0, 300) : ''"
|
:description="item.description ? item.description.substring(0, 300) : ''"
|
||||||
:active="route.params.id === item.id"
|
:active="route.params.id === item.id"
|
||||||
:to="`/tickets/${item.expand.type.id}/${item.id}`"
|
:to="`/tickets/${item.type}/${item.id}`"
|
||||||
:open="item.open"
|
:open="item.open"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { TabsContent } from '@/components/ui/tabs'
|
import { TabsContent } from '@/components/ui/tabs'
|
||||||
|
|
||||||
@@ -12,10 +11,8 @@ defineProps<{
|
|||||||
<TabsContent :value="value" class="flex-1 overflow-hidden">
|
<TabsContent :value="value" class="flex-1 overflow-hidden">
|
||||||
<div class="flex h-full flex-col overflow-hidden">
|
<div class="flex h-full flex-col overflow-hidden">
|
||||||
<Separator class="mt-2" />
|
<Separator class="mt-2" />
|
||||||
<ScrollArea class="flex-1">
|
<slot />
|
||||||
<slot />
|
<div class="h-4" />
|
||||||
<div class="h-4" />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import TicketPanel from '@/components/ticket/TicketPanel.vue'
|
import TicketPanel from '@/components/ticket/TicketPanel.vue'
|
||||||
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
|
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -28,7 +28,12 @@ const dialogOpen = ref(false)
|
|||||||
>
|
>
|
||||||
No links added yet.
|
No links added yet.
|
||||||
</div>
|
</div>
|
||||||
<PanelListElement v-for="link in links" :key="link.id" :title="link.url" class="pr-1">
|
<PanelListElement
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.id"
|
||||||
|
:title="link.url"
|
||||||
|
class="flex-row items-center pr-1"
|
||||||
|
>
|
||||||
<a :href="link.url" target="_blank" class="flex flex-1 items-center overflow-hidden">
|
<a :href="link.url" target="_blank" class="flex flex-1 items-center overflow-hidden">
|
||||||
<span class="mr-2 text-blue-500 underline">
|
<span class="mr-2 text-blue-500 underline">
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
|
||||||
import UserSelect from '@/components/common/UserSelect.vue'
|
import UserSelect from '@/components/common/UserSelect.vue'
|
||||||
import DynamicInput from '@/components/input/DynamicInput.vue'
|
import DynamicInput from '@/components/input/DynamicInput.vue'
|
||||||
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
|
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
@@ -67,12 +67,14 @@ const updateTaskName = (id: string, name: string) => updateTaskNameMutation.muta
|
|||||||
</Card>
|
</Card>
|
||||||
<Card v-else>
|
<Card v-else>
|
||||||
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
|
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
|
||||||
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
<div class="flex flex-row items-center">
|
||||||
<DynamicInput
|
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
||||||
:modelValue="task.name"
|
<DynamicInput
|
||||||
@update:modelValue="updateTaskName(task.id, $event)"
|
:modelValue="task.name"
|
||||||
class="mr-2 flex-1"
|
@update:modelValue="updateTaskName(task.id, $event)"
|
||||||
/>
|
class="mr-2 flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="ml-auto flex items-center">
|
<div class="ml-auto flex items-center">
|
||||||
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
|
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
|
||||||
<Button variant="outline" role="combobox" class="h-8">
|
<Button variant="outline" role="combobox" class="h-8">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
|
||||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
||||||
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
AccordionRoot,
|
|
||||||
type AccordionRootEmits,
|
|
||||||
type AccordionRootProps,
|
|
||||||
useForwardPropsEmits
|
|
||||||
} from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<AccordionRootProps>()
|
|
||||||
const emits = defineEmits<AccordionRootEmits>()
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AccordionRoot v-bind="forwarded">
|
|
||||||
<slot />
|
|
||||||
</AccordionRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AccordionContent, type AccordionContentProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AccordionContent
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
>
|
|
||||||
<div :class="cn('pb-4 pt-0', props.class)">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
|
|
||||||
<slot />
|
|
||||||
</AccordionItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AccordionHeader class="flex">
|
|
||||||
<AccordionTrigger
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
<!-- slot name="icon">
|
|
||||||
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
||||||
</slot-->
|
|
||||||
</AccordionTrigger>
|
|
||||||
</AccordionHeader>
|
|
||||||
</template>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { default as Accordion } from './Accordion.vue'
|
|
||||||
export { default as AccordionContent } from './AccordionContent.vue'
|
|
||||||
export { default as AccordionItem } from './AccordionItem.vue'
|
|
||||||
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { type AvatarVariants, avatarVariant } from '.'
|
|
||||||
import { AvatarRoot } from 'radix-vue'
|
|
||||||
import type { HTMLAttributes } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
class?: HTMLAttributes['class']
|
|
||||||
size?: AvatarVariants['size']
|
|
||||||
shape?: AvatarVariants['shape']
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
size: 'sm',
|
|
||||||
shape: 'circle'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
|
||||||
<slot />
|
|
||||||
</AvatarRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<AvatarFallbackProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AvatarFallback v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</AvatarFallback>
|
|
||||||
</template>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<AvatarImageProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
|
||||||
</template>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { type VariantProps, cva } from 'class-variance-authority'
|
|
||||||
|
|
||||||
export { default as Avatar } from './Avatar.vue'
|
|
||||||
export { default as AvatarImage } from './AvatarImage.vue'
|
|
||||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
|
||||||
|
|
||||||
export const avatarVariant = cva(
|
|
||||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
sm: 'h-10 w-10 text-xs',
|
|
||||||
base: 'h-16 w-16 text-2xl',
|
|
||||||
lg: 'h-32 w-32 text-5xl'
|
|
||||||
},
|
|
||||||
shape: {
|
|
||||||
circle: 'rounded-full',
|
|
||||||
square: 'rounded-md'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
CalendarCell,
|
|
||||||
CalendarCellTrigger,
|
|
||||||
CalendarGrid,
|
|
||||||
CalendarGridBody,
|
|
||||||
CalendarGridHead,
|
|
||||||
CalendarGridRow,
|
|
||||||
CalendarHeadCell,
|
|
||||||
CalendarHeader,
|
|
||||||
CalendarHeading,
|
|
||||||
CalendarNextButton,
|
|
||||||
CalendarPrevButton
|
|
||||||
} from '.'
|
|
||||||
import {
|
|
||||||
CalendarRoot,
|
|
||||||
type CalendarRootEmits,
|
|
||||||
type CalendarRootProps,
|
|
||||||
useForwardPropsEmits
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const emits = defineEmits<CalendarRootEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarRoot v-slot="{ grid, weekDays }" :class="cn('p-3', props.class)" v-bind="forwarded">
|
|
||||||
<CalendarHeader>
|
|
||||||
<CalendarPrevButton />
|
|
||||||
<CalendarHeading />
|
|
||||||
<CalendarNextButton />
|
|
||||||
</CalendarHeader>
|
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
|
||||||
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
|
||||||
<CalendarGridHead>
|
|
||||||
<CalendarGridRow>
|
|
||||||
<CalendarHeadCell v-for="day in weekDays" :key="day">
|
|
||||||
{{ day }}
|
|
||||||
</CalendarHeadCell>
|
|
||||||
</CalendarGridRow>
|
|
||||||
</CalendarGridHead>
|
|
||||||
<CalendarGridBody>
|
|
||||||
<CalendarGridRow
|
|
||||||
v-for="(weekDates, index) in month.rows"
|
|
||||||
:key="`weekDate-${index}`"
|
|
||||||
class="mt-2 w-full"
|
|
||||||
>
|
|
||||||
<CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
|
||||||
<CalendarCellTrigger :day="weekDate" :month="month.value" />
|
|
||||||
</CalendarCell>
|
|
||||||
</CalendarGridRow>
|
|
||||||
</CalendarGridBody>
|
|
||||||
</CalendarGrid>
|
|
||||||
</div>
|
|
||||||
</CalendarRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarCell
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarCell>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarCellTrigger
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
buttonVariants({ variant: 'ghost' }),
|
|
||||||
'h-9 w-9 p-0 font-normal',
|
|
||||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
|
||||||
// Selected
|
|
||||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
|
||||||
// Disabled
|
|
||||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
|
||||||
// Unavailable
|
|
||||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
|
||||||
// Outside months
|
|
||||||
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarCellTrigger>
|
|
||||||
</template>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGrid
|
|
||||||
:class="cn('w-full border-collapse space-y-1', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarGrid>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridBodyProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridBody v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridBody>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridHeadProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridHead v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridHead>
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
|
||||||
<slot />
|
|
||||||
</CalendarGridRow>
|
|
||||||
</template>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeadCell
|
|
||||||
:class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarHeadCell>
|
|
||||||
</template>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeader
|
|
||||||
:class="cn('relative flex w-full items-center justify-between pt-1', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CalendarHeader>
|
|
||||||
</template>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarHeading
|
|
||||||
v-slot="{ headingValue }"
|
|
||||||
:class="cn('text-sm font-medium', props.class)"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot :heading-value>
|
|
||||||
{{ headingValue }}
|
|
||||||
</slot>
|
|
||||||
</CalendarHeading>
|
|
||||||
</template>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
import { ChevronRight } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarNext
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
buttonVariants({ variant: 'outline' }),
|
|
||||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
<ChevronRight class="h-4 w-4" />
|
|
||||||
</slot>
|
|
||||||
</CalendarNext>
|
|
||||||
</template>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
|
|
||||||
import { ChevronLeft } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwardedProps = useForwardProps(delegatedProps)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CalendarPrev
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
buttonVariants({ variant: 'outline' }),
|
|
||||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
v-bind="forwardedProps"
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
<ChevronLeft class="h-4 w-4" />
|
|
||||||
</slot>
|
|
||||||
</CalendarPrev>
|
|
||||||
</template>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export { default as Calendar } from './Calendar.vue'
|
|
||||||
export { default as CalendarCell } from './CalendarCell.vue'
|
|
||||||
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
|
|
||||||
export { default as CalendarGrid } from './CalendarGrid.vue'
|
|
||||||
export { default as CalendarGridBody } from './CalendarGridBody.vue'
|
|
||||||
export { default as CalendarGridHead } from './CalendarGridHead.vue'
|
|
||||||
export { default as CalendarGridRow } from './CalendarGridRow.vue'
|
|
||||||
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
|
|
||||||
export { default as CalendarHeader } from './CalendarHeader.vue'
|
|
||||||
export { default as CalendarHeading } from './CalendarHeading.vue'
|
|
||||||
export { default as CalendarNextButton } from './CalendarNextButton.vue'
|
|
||||||
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue'
|
|
||||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<CollapsibleRootProps>()
|
|
||||||
const emits = defineEmits<CollapsibleRootEmits>()
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
|
|
||||||
<slot :open="open" />
|
|
||||||
</CollapsibleRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<CollapsibleContentProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CollapsibleContent
|
|
||||||
v-bind="props"
|
|
||||||
class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</CollapsibleContent>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue'
|
|
||||||
|
|
||||||
const props = defineProps<CollapsibleTriggerProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CollapsibleTrigger v-bind="props">
|
|
||||||
<slot />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default as Collapsible } from './Collapsible.vue'
|
|
||||||
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
|
|
||||||
export { default as CollapsibleContent } from './CollapsibleContent.vue'
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { GripVertical } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
import {
|
|
||||||
SplitterResizeHandle,
|
|
||||||
type SplitterResizeHandleEmits,
|
|
||||||
type SplitterResizeHandleProps,
|
|
||||||
useForwardPropsEmits
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
SplitterResizeHandleProps & { class?: HTMLAttributes['class']; withHandle?: boolean }
|
|
||||||
>()
|
|
||||||
const emits = defineEmits<SplitterResizeHandleEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SplitterResizeHandle
|
|
||||||
v-bind="forwarded"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]>div]:rotate-90 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template v-if="props.withHandle">
|
|
||||||
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
|
||||||
<GripVertical class="h-2.5 w-2.5" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</SplitterResizeHandle>
|
|
||||||
</template>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
SplitterGroup,
|
|
||||||
type SplitterGroupEmits,
|
|
||||||
type SplitterGroupProps,
|
|
||||||
useForwardPropsEmits
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
const emits = defineEmits<SplitterGroupEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SplitterGroup
|
|
||||||
v-bind="forwarded"
|
|
||||||
:class="cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</SplitterGroup>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'
|
|
||||||
export { default as ResizableHandle } from './ResizableHandle.vue'
|
|
||||||
export { SplitterPanel as ResizablePanel } from 'radix-vue'
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import ScrollBar from './ScrollBar.vue'
|
|
||||||
import {
|
|
||||||
ScrollAreaCorner,
|
|
||||||
ScrollAreaRoot,
|
|
||||||
type ScrollAreaRootProps,
|
|
||||||
ScrollAreaViewport
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
|
|
||||||
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
|
|
||||||
<slot />
|
|
||||||
</ScrollAreaViewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaCorner />
|
|
||||||
</ScrollAreaRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb } from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(),
|
|
||||||
{
|
|
||||||
orientation: 'vertical'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ScrollAreaScrollbar
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'flex touch-none select-none transition-colors',
|
|
||||||
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
|
|
||||||
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaScrollbar>
|
|
||||||
</template>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as ScrollArea } from './ScrollArea.vue'
|
|
||||||
export { default as ScrollBar } from './ScrollBar.vue'
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
SwitchRoot,
|
|
||||||
type SwitchRootEmits,
|
|
||||||
type SwitchRootProps,
|
|
||||||
SwitchThumb,
|
|
||||||
useForwardPropsEmits
|
|
||||||
} from 'radix-vue'
|
|
||||||
import { type HTMLAttributes, computed } from 'vue'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
|
|
||||||
|
|
||||||
const emits = defineEmits<SwitchRootEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props
|
|
||||||
|
|
||||||
return delegated
|
|
||||||
})
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SwitchRoot
|
|
||||||
v-bind="forwarded"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
|
||||||
props.class
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<SwitchThumb
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</SwitchRoot>
|
|
||||||
</template>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as Switch } from './Switch.vue'
|
|
||||||
@@ -32,6 +32,16 @@ export interface Ticket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchTicket {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
created: string
|
||||||
|
description: string
|
||||||
|
open: boolean
|
||||||
|
type: string
|
||||||
|
owner_name: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
@@ -126,6 +136,7 @@ export interface JSONSchema {
|
|||||||
title: string
|
title: string
|
||||||
type: string
|
type: string
|
||||||
description?: string
|
description?: string
|
||||||
|
enum?: Array<string>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
required?: Array<string>
|
required?: Array<string>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
|
|
||||||
import DashboardView from '@/views/DashboardView.vue'
|
import DashboardView from '@/views/DashboardView.vue'
|
||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
|
import PasswordResetView from '@/views/PasswordResetView.vue'
|
||||||
import ReactionView from '@/views/ReactionView.vue'
|
import ReactionView from '@/views/ReactionView.vue'
|
||||||
import TicketView from '@/views/TicketView.vue'
|
import TicketView from '@/views/TicketView.vue'
|
||||||
|
|
||||||
@@ -12,25 +13,30 @@ const router = createRouter({
|
|||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/dashboard'
|
redirect: '/dashboard'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/reactions/:id?',
|
|
||||||
name: 'reactions',
|
|
||||||
component: ReactionView
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
component: DashboardView
|
component: DashboardView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tickets/:type/:id?',
|
||||||
|
name: 'tickets',
|
||||||
|
component: TicketView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reactions/:id?',
|
||||||
|
name: 'reactions',
|
||||||
|
component: ReactionView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: LoginView
|
component: LoginView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tickets/:type/:id?',
|
path: '/password-reset',
|
||||||
name: 'tickets',
|
name: 'password-reset',
|
||||||
component: TicketView
|
component: PasswordResetView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import OpenTasks from '@/components/dashboard/OpenTasks.vue'
|
|||||||
import OpenTickets from '@/components/dashboard/OpenTickets.vue'
|
import OpenTickets from '@/components/dashboard/OpenTickets.vue'
|
||||||
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
||||||
import TicketTypes from '@/components/dashboard/TicketTypes.vue'
|
import TicketTypes from '@/components/dashboard/TicketTypes.vue'
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
|
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import TwoColumn from '@/components/layout/TwoColumn.vue'
|
import TwoColumn from '@/components/layout/TwoColumn.vue'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
|
||||||
import { ExternalLink } from 'lucide-vue-next'
|
import { ExternalLink } from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -46,96 +47,91 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TwoColumn>
|
<TwoColumn>
|
||||||
<div class="flex h-screen flex-1 flex-col">
|
<ColumnHeader title="Dashboard" />
|
||||||
<div class="flex h-14 min-h-14 items-center bg-background px-4 py-2">
|
<ColumnBody>
|
||||||
<h1 class="text-xl font-bold">Dashboard</h1>
|
<ColumnBodyContainer
|
||||||
</div>
|
class="grid grid-cols-1 grid-rows-[100px_100px_100px_100px] md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
|
||||||
<Separator class="shrink-0" />
|
>
|
||||||
<ScrollArea>
|
<Card>
|
||||||
<div
|
<CardHeader>
|
||||||
class="m-auto grid max-w-7xl grid-cols-1 grid-rows-[100px_100px_100px_100px] gap-4 p-4 md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
|
<CardTitle>{{ count('tasks') }}</CardTitle>
|
||||||
>
|
<CardDescription>Tasks</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('tasks') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Tasks</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('tickets') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Tickets</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('tickets') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Tickets</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('users') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Users</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('users') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Users</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('reactions') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Reactions</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle></CardTitle>
|
<Card>
|
||||||
<CardDescription></CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle> Catalyst</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card>
|
<CardContent class="flex flex-1 flex-col gap-1">
|
||||||
<CardHeader>
|
<a
|
||||||
<CardTitle> Catalyst</CardTitle>
|
href="https://catalyst.security-brewery.com/docs/category/catalyst-handbook"
|
||||||
</CardHeader>
|
target="_blank"
|
||||||
<CardContent class="flex flex-1 flex-col gap-1">
|
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||||
<a
|
>
|
||||||
href="https://catalyst-soar.com/docs/category/catalyst-handbook"
|
Open Catalyst Handbook
|
||||||
target="_blank"
|
<ExternalLink class="ml-2 h-4 w-4" />
|
||||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
</a>
|
||||||
>
|
<a
|
||||||
Open Catalyst Handbook
|
href="/_/"
|
||||||
<ExternalLink class="ml-2 h-4 w-4" />
|
target="_blank"
|
||||||
</a>
|
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||||
<a
|
>
|
||||||
href="/_/"
|
Open Admin Interface
|
||||||
target="_blank"
|
<ExternalLink class="ml-2 h-4 w-4" />
|
||||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
</a>
|
||||||
>
|
</CardContent>
|
||||||
Open Admin Interface
|
</Card>
|
||||||
<ExternalLink class="ml-2 h-4 w-4" />
|
<Card>
|
||||||
</a>
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle> Tickets by Type</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card>
|
<CardContent>
|
||||||
<CardHeader>
|
<TicketTypes />
|
||||||
<CardTitle> Tickets by Type</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<TicketTypes />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Tickets Per Week</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<TicketOverTime />
|
||||||
<CardTitle>Tickets Per Week</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<TicketOverTime />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Your Open Tickets</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<OpenTickets />
|
||||||
<CardTitle>Your Open Tickets</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<OpenTickets />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Your Open Tasks</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<OpenTasks />
|
||||||
<CardTitle>Your Open Tasks</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
</ColumnBodyContainer>
|
||||||
<OpenTasks />
|
</ColumnBody>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</TwoColumn>
|
</TwoColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
@@ -8,9 +9,11 @@ import { useQuery } from '@tanstack/vue-query'
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const mail = ref('')
|
const mail = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
const errorTitle = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
@@ -20,6 +23,7 @@ const login = () => {
|
|||||||
window.location.href = '/ui/'
|
window.location.href = '/ui/'
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
errorTitle.value = 'Login failed'
|
||||||
errorMessage.value = error.message
|
errorMessage.value = error.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,7 +41,8 @@ watch(
|
|||||||
mail.value = 'user@catalyst-soar.com'
|
mail.value = 'user@catalyst-soar.com'
|
||||||
password.value = '1234567890'
|
password.value = '1234567890'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,12 +50,18 @@ watch(
|
|||||||
<div class="flex h-full w-full flex-1 items-center justify-center">
|
<div class="flex h-full w-full flex-1 items-center justify-center">
|
||||||
<Card class="m-auto w-96">
|
<Card class="m-auto w-96">
|
||||||
<CardHeader class="flex flex-row justify-between">
|
<CardHeader class="flex flex-row justify-between">
|
||||||
<CardTitle>Catalyst</CardTitle>
|
<CardTitle class="flex flex-row">
|
||||||
|
<CatalystLogo class="size-12" />
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-bold">Catalyst</h1>
|
||||||
|
<div class="text-muted-foreground">Login</div>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-col gap-4">
|
<CardContent class="flex flex-col gap-4">
|
||||||
<Alert v-if="errorMessage" variant="destructive" class="border-4 p-4">
|
<Alert v-if="errorTitle || errorMessage" variant="destructive" class="border-4 p-4">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle v-if="errorTitle">{{ errorTitle }}</AlertTitle>
|
||||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
<AlertDescription v-if="errorMessage">{{ errorMessage }}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Input
|
<Input
|
||||||
v-model="mail"
|
v-model="mail"
|
||||||
@@ -66,7 +77,14 @@ watch(
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
@keydown.enter="login"
|
@keydown.enter="login"
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" class="w-full" @click="login"> Login</Button>
|
<Button variant="outline" class="w-full" @click="login">Login</Button>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'password-reset' }"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'link', size: 'default' }), 'w-full text-foreground')
|
||||||
|
"
|
||||||
|
>Reset Password
|
||||||
|
</RouterLink>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
79
ui/src/views/PasswordResetView.vue
Normal file
79
ui/src/views/PasswordResetView.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CatalystLogo from '@/components/common/CatalystLogo.vue'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { pb } from '@/lib/pocketbase'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface AlertData {
|
||||||
|
variant: 'default' | 'destructive'
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const mail = ref('')
|
||||||
|
const alert = ref<AlertData | null>(null)
|
||||||
|
|
||||||
|
const resetPassword = () => {
|
||||||
|
pb.collection('users')
|
||||||
|
.requestPasswordReset(mail.value)
|
||||||
|
.then(() => {
|
||||||
|
alert.value = {
|
||||||
|
variant: 'default',
|
||||||
|
title: 'Password reset',
|
||||||
|
message: 'Password reset email sent'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert.value = {
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'Password reset failed',
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full w-full flex-1 items-center justify-center">
|
||||||
|
<Card class="m-auto w-96">
|
||||||
|
<CardHeader class="flex flex-row justify-between">
|
||||||
|
<CardTitle class="flex flex-row">
|
||||||
|
<CatalystLogo class="size-12" />
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-bold">Catalyst</h1>
|
||||||
|
<div class="text-muted-foreground">Password Reset</div>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-col gap-4">
|
||||||
|
<Alert v-if="alert" :variant="alert.variant" class="border-4 p-4">
|
||||||
|
<AlertTitle>{{ alert.title }}</AlertTitle>
|
||||||
|
<AlertDescription>{{ alert.message }}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<div v-else class="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
v-model="mail"
|
||||||
|
type="text"
|
||||||
|
placeholder="Email"
|
||||||
|
class="w-full"
|
||||||
|
@keydown.enter="resetPassword"
|
||||||
|
/>
|
||||||
|
<Button variant="outline" class="w-full" @click="resetPassword">Reset Password</Button>
|
||||||
|
</div>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'login' }"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'link', size: 'default' }), 'w-full text-foreground')
|
||||||
|
"
|
||||||
|
>Back to Login
|
||||||
|
</RouterLink>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts" xmlns="http://www.w3.org/1999/html">
|
<script setup lang="ts">
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||||
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
|
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
|
||||||
import ReactionList from '@/components/reaction/ReactionList.vue'
|
import ReactionList from '@/components/reaction/ReactionList.vue'
|
||||||
@@ -22,14 +23,14 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThreeColumn>
|
<ThreeColumn :show-details="!!id">
|
||||||
<template #list>
|
<template #list>
|
||||||
<ReactionList />
|
<ReactionList />
|
||||||
</template>
|
</template>
|
||||||
<template #single>
|
<template #single>
|
||||||
<div v-if="!id" class="flex h-full w-full items-center justify-center text-lg text-gray-500">
|
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
|
||||||
No reaction selected
|
No reaction selected
|
||||||
</div>
|
</ColumnBody>
|
||||||
<ReactionNew v-else-if="id === 'new'" key="new" />
|
<ReactionNew v-else-if="id === 'new'" key="new" />
|
||||||
<ReactionDisplay v-else :key="id" :id="id" />
|
<ReactionDisplay v-else :key="id" :id="id" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||||
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
||||||
import TicketList from '@/components/ticket/TicketList.vue'
|
import TicketList from '@/components/ticket/TicketList.vue'
|
||||||
@@ -41,20 +42,17 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThreeColumn>
|
<ThreeColumn :show-details="!!id">
|
||||||
<template #list>
|
<template #list>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
|
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
<template #single>
|
<template #single>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div
|
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
|
||||||
v-if="!id"
|
|
||||||
class="flex h-full w-full items-center justify-center text-lg text-gray-500"
|
|
||||||
>
|
|
||||||
No ticket selected
|
No ticket selected
|
||||||
</div>
|
</ColumnBody>
|
||||||
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user