forked from tangled.org/core
Monorepo for Tangled

appview/pages: display DID format labels handles

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li d66a4745 a20273b1

verified
Changed files
+91 -67
appview
labels
pages
templates
repo
+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
··· 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
··· 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
··· 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
··· 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
··· 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>