+25
-24
appview/labels/labels.go
+25
-24
appview/labels/labels.go
···
15
15
"github.com/go-chi/chi/v5"
16
16
17
17
"tangled.sh/tangled.sh/core/api/tangled"
18
-
"tangled.sh/tangled.sh/core/appview/config"
19
18
"tangled.sh/tangled.sh/core/appview/db"
20
19
"tangled.sh/tangled.sh/core/appview/middleware"
21
20
"tangled.sh/tangled.sh/core/appview/oauth"
22
21
"tangled.sh/tangled.sh/core/appview/pages"
23
-
"tangled.sh/tangled.sh/core/appview/reporesolver"
22
+
"tangled.sh/tangled.sh/core/appview/validator"
24
23
"tangled.sh/tangled.sh/core/appview/xrpcclient"
25
-
"tangled.sh/tangled.sh/core/eventconsumer"
26
-
"tangled.sh/tangled.sh/core/idresolver"
27
24
"tangled.sh/tangled.sh/core/log"
28
-
"tangled.sh/tangled.sh/core/rbac"
29
25
"tangled.sh/tangled.sh/core/tid"
30
26
)
31
27
32
28
type Labels struct {
33
-
repoResolver *reporesolver.RepoResolver
34
-
idResolver *idresolver.Resolver
35
-
oauth *oauth.OAuth
36
-
pages *pages.Pages
37
-
db *db.DB
38
-
logger *slog.Logger
29
+
oauth *oauth.OAuth
30
+
pages *pages.Pages
31
+
db *db.DB
32
+
logger *slog.Logger
33
+
validator *validator.Validator
39
34
}
40
35
41
36
func New(
42
37
oauth *oauth.OAuth,
43
-
repoResolver *reporesolver.RepoResolver,
44
38
pages *pages.Pages,
45
-
spindlestream *eventconsumer.Consumer,
46
-
idResolver *idresolver.Resolver,
47
39
db *db.DB,
48
-
config *config.Config,
49
-
enforcer *rbac.Enforcer,
40
+
validator *validator.Validator,
50
41
) *Labels {
51
42
logger := log.New("labels")
52
43
53
44
return &Labels{
54
-
oauth: oauth,
55
-
repoResolver: repoResolver,
56
-
pages: pages,
57
-
idResolver: idResolver,
58
-
db: db,
59
-
logger: logger,
45
+
oauth: oauth,
46
+
pages: pages,
47
+
db: db,
48
+
logger: logger,
49
+
validator: validator,
60
50
}
61
51
}
62
52
···
107
97
})
108
98
}
109
99
110
-
// TODO: validate the operations
111
-
112
100
// find all the labels that this repo subscribes to
113
101
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
114
102
if err != nil {
···
127
115
return
128
116
}
129
117
118
+
for i := range labelOps {
119
+
def := actx.Defs[labelOps[i].OperandKey]
120
+
if err := l.validator.ValidateLabelOp(def, &labelOps[i]); err != nil {
121
+
l.logger.Error("form failed to validate", "err", err)
122
+
http.Error(w, "Invalid form data", http.StatusBadRequest)
123
+
return
124
+
}
125
+
126
+
l.logger.Info("value changed to: ", "v", labelOps[i].OperandValue)
127
+
}
128
+
130
129
// calculate the start state by applying already known labels
131
130
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
132
131
if err != nil {
···
136
135
137
136
labelState := db.NewLabelState()
138
137
actx.ApplyLabelOps(labelState, existingOps)
138
+
139
+
l.logger.Info("state", "state", labelState)
139
140
140
141
// next, apply all ops introduced in this request and filter out ones that are no-ops
141
142
validLabelOps := labelOps[:0]
+28
-20
appview/pages/templates/repo/fragments/addLabelModal.html
+28
-20
appview/pages/templates/repo/fragments/addLabelModal.html
···
5
5
{{ with $root }}
6
6
<form
7
7
hx-put="/{{ .RepoInfo.FullName }}/labels/perform"
8
-
hx-on::after-request="if(event.detail.successful) this.reset()"
8
+
hx-on::after-request="this.reset()"
9
9
hx-indicator="#spinner"
10
10
hx-swap="none"
11
11
class="flex flex-col gap-4"
···
15
15
<input class="hidden" name="repo" value="{{ .RepoInfo.RepoAt.String }}">
16
16
<input class="hidden" name="subject" value="{{ $subject }}">
17
17
18
-
<div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
18
+
<div class="flex flex-col gap-2">
19
19
{{ $id := 0 }}
20
20
{{ range $k, $valset := $state.Inner }}
21
21
{{ $d := index $root.LabelDefs $k }}
22
22
{{ range $v, $s := $valset }}
23
-
<div class="grid grid-cols-2 cursor-pointer rounded">
24
-
<label class="w-full flex items-center gap-2">
25
-
<input type="checkbox" name="op-{{$id}}" value="add" checked>
26
-
{{ template "labels/fragments/labelDef" $d }}
27
-
</label>
28
-
{{ template "valueTypeInput" (dict "valueType" $d.ValueType "value" $v "key" $k) }}
29
-
<input type="hidden" name="operand-key" value="{{ $k }}">
30
-
{{ $id = add $id 1 }}
31
-
</div>
23
+
{{ template "labelCheckbox" (dict "def" $d "key" $k "val" $v "id" $id "isChecked" true) }}
24
+
{{ $id = add $id 1 }}
32
25
{{ end }}
33
26
{{ end }}
34
27
35
28
{{ range $k, $d := $root.LabelDefs }}
36
29
{{ if not ($state.ContainsLabel $k) }}
37
-
<div class="grid grid-cols-2 cursor-pointer rounded">
38
-
<label class="w-full flex items-center gap-2">
39
-
<input type="checkbox" name="op-{{$id}}" value="add">
40
-
{{ template "labels/fragments/labelDef" $d }}
41
-
</label>
42
-
{{ template "valueTypeInput" (dict "valueType" $d.ValueType "value" "" "key" $k) }}
43
-
<input type="hidden" name="operand-key" value="{{ $k }}">
44
-
{{ $id = add $id 1 }}
45
-
</div>
30
+
{{ template "labelCheckbox" (dict "def" $d "key" $k "val" "" "id" $id "isChecked" false) }}
31
+
{{ $id = add $id 1 }}
46
32
{{ end }}
33
+
{{ else }}
34
+
<span>
35
+
No labels defined yet. You can define custom labels in <a class="underline" href="/{{ .RepoInfo.FullName }}/settings">settings</a>.
36
+
</span>
47
37
{{ end }}
48
38
</div>
49
39
···
68
58
{{ end }}
69
59
{{ end }}
70
60
61
+
{{ define "labelCheckbox" }}
62
+
{{ $key := .key }}
63
+
{{ $val := .val }}
64
+
{{ $def := .def }}
65
+
{{ $id := .id }}
66
+
{{ $isChecked := .isChecked }}
67
+
<div class="grid grid-cols-[auto_1fr_50%] gap-2 items-center cursor-pointer">
68
+
<input type="checkbox" id="op-{{$id}}" name="op-{{$id}}" value="add" {{if $isChecked}}checked{{end}} class="peer">
69
+
<label for="op-{{$id}}" class="flex items-center gap-2 text-base">{{ template "labels/fragments/labelDef" $def }}</label>
70
+
<div class="w-full hidden peer-checked:block">{{ template "valueTypeInput" (dict "valueType" $def.ValueType "value" $val "key" $key) }}</div>
71
+
<input type="hidden" name="operand-key" value="{{ $key }}">
72
+
</div>
73
+
{{ end }}
74
+
71
75
{{ define "valueTypeInput" }}
72
76
{{ $valueType := .valueType }}
73
77
{{ $value := .value }}
···
110
114
{{ end }}
111
115
112
116
{{ define "stringTypeInput" }}
117
+
{{ $valueType := .valueType }}
113
118
{{ $value := .value }}
119
+
{{ if $valueType.IsDidFormat }}
120
+
{{ $value = resolve .value }}
121
+
{{ end }}
114
122
<input class="p-1 w-full" type="text" name="operand-val" value="{{$value}}">
115
123
{{ end }}
116
124
+4
-4
appview/pages/templates/repo/issues/fragments/commentList.html
+4
-4
appview/pages/templates/repo/issues/fragments/commentList.html
···
3
3
{{ range $item := .CommentList }}
4
4
{{ template "commentListing" (list $ .) }}
5
5
{{ end }}
6
-
<div>
6
+
</div>
7
7
{{ end }}
8
8
9
9
{{ define "commentListing" }}
···
16
16
"Issue" $root.Issue
17
17
"Comment" $comment.Self) }}
18
18
19
-
<div class="rounded border border-gray-300 dark:border-gray-700 w-full overflow-hidden shadow-sm">
19
+
<div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50">
20
20
{{ template "topLevelComment" $params }}
21
21
22
-
<div class="relative ml-4 border-l border-gray-300 dark:border-gray-700">
22
+
<div class="relative ml-4 border-l-2 border-gray-200 dark:border-gray-700">
23
23
{{ range $index, $reply := $comment.Replies }}
24
24
<div class="relative ">
25
25
<!-- Horizontal connector -->
26
-
<div class="absolute left-0 top-6 w-4 h-px bg-gray-300 dark:bg-gray-700"></div>
26
+
<div class="absolute left-0 top-6 w-4 h-1 bg-gray-200 dark:bg-gray-700"></div>
27
27
28
28
<div class="pl-2">
29
29
{{
+13
-4
appview/pages/templates/repo/settings/fragments/addLabelDefModal.html
+13
-4
appview/pages/templates/repo/settings/fragments/addLabelDefModal.html
···
3
3
hx-put="/{{ $.RepoInfo.FullName }}/settings/label"
4
4
hx-indicator="#spinner"
5
5
hx-swap="none"
6
+
hx-on::after-request="if(event.detail.successful) this.reset()"
6
7
class="flex flex-col gap-4"
7
8
>
8
9
<p class="text-gray-500 dark:text-gray-400">Labels can have a name and a value. Set the value type to "none" to create a simple label.</p>
···
16
17
<div class="w-full">
17
18
<label for="valueType">Value Type</label>
18
19
<select id="value-type" name="valueType" class="w-full p-3 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
19
-
<option value="string" selected>String</option>
20
+
<option value="null" selected>None</option>
21
+
<option value="string">String</option>
20
22
<option value="integer">Integer</option>
21
23
<option value="boolean">Boolean</option>
22
-
<option value="null">None</option>
23
24
</select>
24
-
<details id="constrain-values" class="group">
25
+
<details id="constrain-values" class="group hidden">
25
26
<summary class="list-none cursor-pointer flex items-center gap-2 py-2">
26
27
<span class="group-open:hidden inline text-gray-500 dark:text-gray-400">{{ i "square-plus" "w-4 h-4" }}</span>
27
28
<span class="hidden group-open:inline text-gray-500 dark:text-gray-400">{{ i "square-minus" "w-4 h-4" }}</span>
28
29
<span>Constrain values</span>
29
30
</summary>
31
+
<label for="enumValues">Permitted values</label>
30
32
<input type="text" id="enumValues" name="enumValues" placeholder="value1, value2, value3" class="w-full"/>
31
33
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Enter comma-separated list of permitted values.</p>
34
+
35
+
<label for="valueFormat">String format</label>
36
+
<select id="valueFormat" name="valueFormat" class="w-full p-3 rounded border border-gray-300 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-600">
37
+
<option value="any" selected>Any</option>
38
+
<option value="did">DID</option>
39
+
</select>
40
+
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Choose a string format.</p>
32
41
</details>
33
42
</div>
34
43
···
87
96
const constrainValues = document.getElementById('constrain-values');
88
97
const selectedValue = this.value;
89
98
90
-
if (selectedValue === 'string' || selectedValue === 'integer') {
99
+
if (selectedValue === 'string') {
91
100
constrainValues.classList.remove('hidden');
92
101
} else {
93
102
constrainValues.classList.add('hidden');
+19
-13
appview/pages/templates/repo/settings/fragments/labelListing.html
+19
-13
appview/pages/templates/repo/settings/fragments/labelListing.html
···
5
5
<div class="flex flex-col gap-1 text-sm min-w-0 max-w-[80%]">
6
6
{{ template "labels/fragments/labelDef" $label }}
7
7
<div class="flex flex-wrap text items-center gap-1 text-gray-500 dark:text-gray-400">
8
-
{{ $label.ValueType.Type }}
8
+
{{ $label.ValueType.Type }} type
9
9
{{ if $label.ValueType.IsEnumType }}
10
10
<span class="before:content-['·'] before:select-none"></span>
11
11
{{ join $label.ValueType.Enum ", " }}
12
12
{{ end }}
13
+
{{ if $label.ValueType.IsDidFormat }}
14
+
<span class="before:content-['·'] before:select-none"></span>
15
+
DID format
16
+
{{ end }}
13
17
</div>
14
18
</div>
15
-
<button
16
-
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
17
-
title="Delete label"
18
-
hx-delete="/{{ $root.RepoInfo.FullName }}/settings/label"
19
-
hx-swap="none"
20
-
hx-vals='{"label-id": "{{ $label.Id }}"}'
21
-
hx-confirm="Are you sure you want to delete the label `{{ $label.Name }}`?"
22
-
>
23
-
{{ i "trash-2" "w-5 h-5" }}
24
-
<span class="hidden md:inline">delete</span>
25
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
26
-
</button>
19
+
{{ if $root.RepoInfo.Roles.IsOwner }}
20
+
<button
21
+
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
22
+
title="Delete label"
23
+
hx-delete="/{{ $root.RepoInfo.FullName }}/settings/label"
24
+
hx-swap="none"
25
+
hx-vals='{"label-id": "{{ $label.Id }}"}'
26
+
hx-confirm="Are you sure you want to delete the label `{{ $label.Name }}`?"
27
+
>
28
+
{{ i "trash-2" "w-5 h-5" }}
29
+
<span class="hidden md:inline">delete</span>
30
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
31
+
</button>
32
+
{{ end }}
27
33
</div>
28
34
{{ end }}
+2
-2
appview/pages/templates/repo/settings/general.html
+2
-2
appview/pages/templates/repo/settings/general.html
···
57
57
<button
58
58
class="btn flex items-center gap-2"
59
59
popovertarget="add-labeldef-modal"
60
-
{{ if not (or .RepoInfo.Roles.IsOwner .RepoInfo.Roles.IsCollaborator) }}disabled{{ end }}
60
+
{{ if not .RepoInfo.Roles.IsOwner }}disabled{{ end }}
61
61
popovertargetaction="toggle">
62
62
{{ i "plus" "size-4" }}
63
63
add label
···
65
65
<div
66
66
id="add-labeldef-modal"
67
67
popover
68
-
class="bg-white w-full sm:w-96 dark:bg-gray-800 p-6 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
68
+
class="bg-white w-full sm:w-[30rem] dark:bg-gray-800 p-6 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
69
69
{{ template "repo/settings/fragments/addLabelDefModal" . }}
70
70
</div>
71
71
</div>