Project edit - editing owners with preview
This commit is contained in:
parent
f706709c34
commit
b591e5f6bd
|
@ -184,11 +184,12 @@ func ThreadToTemplate(t *models.Thread) Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserAvatarDefaultUrl(theme string) string {
|
func UserAvatarDefaultUrl(theme string) string {
|
||||||
|
// TODO(redesign): Get rid of theme here
|
||||||
return hmnurl.BuildTheme("empty-avatar.svg", theme, true)
|
return hmnurl.BuildTheme("empty-avatar.svg", theme, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UserAvatarUrl(u *models.User) string {
|
func UserAvatarUrl(u *models.User) string {
|
||||||
avatar := ""
|
avatar := UserAvatarDefaultUrl("light")
|
||||||
if u != nil && u.AvatarAsset != nil {
|
if u != nil && u.AvatarAsset != nil {
|
||||||
avatar = hmnurl.BuildS3Asset(u.AvatarAsset.S3Key)
|
avatar = hmnurl.BuildS3Asset(u.AvatarAsset.S3Key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,18 +145,24 @@
|
||||||
<div id="owners_error" class="f6"></div>
|
<div id="owners_error" class="f6"></div>
|
||||||
<div id="owner_list" class="pt3 flex flex-wrap g3">
|
<div id="owner_list" class="pt3 flex flex-wrap g3">
|
||||||
<template id="owner_row">
|
<template id="owner_row">
|
||||||
<div class="owner_row flex flex-row items-center bg2 pa2" data-tmpl="root">
|
<div class="owner_row flex flex-row items-center g2 bg3 pa2" data-tmpl="root">
|
||||||
<input type="hidden" name="owners" data-tmpl="input" />
|
<input type="hidden" name="owners" data-tmpl="input" />
|
||||||
<span data-tmpl="name"></span>
|
<div class="flex g1 items-center b">
|
||||||
<a class="remove_owner svgicon f7 link-normal pl2" href="javascript:;">{{ svg "close" }}</a>
|
<img data-tmpl="avatar" class="avatar avatar-user avatar-small" src="" />
|
||||||
|
<span data-tmpl="name"></span>
|
||||||
|
</div>
|
||||||
|
<a class="remove_owner svgicon f7 link-normal" href="javascript:;">{{ svg "close" }}</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{ range .ProjectSettings.Owners }}
|
{{ range .ProjectSettings.Owners }}
|
||||||
<div class="owner_row flex flex-row items-center bg2 pa2">
|
<div class="owner_row flex flex-row items-center g2 bg3 pa2">
|
||||||
<input type="hidden" name="owners" value="{{ .Username }}" />
|
<input type="hidden" name="owners" value="{{ .Username }}" />
|
||||||
<span>{{ .Username }}</span>
|
<div class="flex g1 items-center b">
|
||||||
|
<img class="avatar avatar-user avatar-small" src="{{ .AvatarUrl }}" />
|
||||||
|
<span title="{{ .Username }}">{{ .Name }}</span>
|
||||||
|
</div>
|
||||||
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
|
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
|
||||||
<a class="remove_owner svgicon f7 link-normal pl2" href="javascript:;">{{ svg "close" }}</a>
|
<a class="remove_owner svgicon f7 link-normal" href="javascript:;">{{ svg "close" }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -267,11 +273,6 @@
|
||||||
<div id="owners_preview_container">
|
<div id="owners_preview_container">
|
||||||
<hr class="mv3">
|
<hr class="mv3">
|
||||||
<div id="owners_preview" class="flex flex-wrap g2">
|
<div id="owners_preview" class="flex flex-wrap g2">
|
||||||
<!-- TODO(redesign): Actually preview owners -->
|
|
||||||
<div class="flex g1 items-center b">
|
|
||||||
<div class="avatar avatar-user avatar-small"></div>
|
|
||||||
<span>Example User</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO(redesign): Preview badges -->
|
<!-- TODO(redesign): Preview badges -->
|
||||||
|
@ -340,6 +341,7 @@
|
||||||
let ownerList = document.querySelector("#owner_list");
|
let ownerList = document.querySelector("#owner_list");
|
||||||
let ownerTemplate = makeTemplateCloner("owner_row");
|
let ownerTemplate = makeTemplateCloner("owner_row");
|
||||||
let ownerPreviewTemplate = makeTemplateCloner("owner_preview");
|
let ownerPreviewTemplate = makeTemplateCloner("owner_preview");
|
||||||
|
let ownersPreviewContainer = document.querySelector("#owners_preview");
|
||||||
|
|
||||||
addOwnerInput.addEventListener("keypress", function(ev) {
|
addOwnerInput.addEventListener("keypress", function(ev) {
|
||||||
if (ev.which == 13) {
|
if (ev.which == 13) {
|
||||||
|
@ -385,7 +387,7 @@
|
||||||
let result = xhr.response;
|
let result = xhr.response;
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
addOwner(result.canonical);
|
addOwner(result.username, result.name, result.avatarUrl);
|
||||||
addOwnerInput.value = "";
|
addOwnerInput.value = "";
|
||||||
} else {
|
} else {
|
||||||
ownersError.textContent = "Username not found";
|
ownersError.textContent = "Username not found";
|
||||||
|
@ -417,21 +419,41 @@
|
||||||
updateAddOwnerStyles();
|
updateAddOwnerStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOwner(username) {
|
function addOwner(username, bestName, avatarUrl) {
|
||||||
let ownerEl = ownerTemplate();
|
let ownerEl = ownerTemplate();
|
||||||
ownerEl.input.value = username;
|
ownerEl.input.value = username;
|
||||||
ownerEl.name.textContent = username;
|
ownerEl.name.textContent = bestName;
|
||||||
|
ownerEl.title = username;
|
||||||
|
ownerEl.avatar.src = avatarUrl;
|
||||||
ownerList.appendChild(ownerEl.root);
|
ownerList.appendChild(ownerEl.root);
|
||||||
updateAddOwnerStyles();
|
updateAddOwnerStyles();
|
||||||
|
updateOwnersPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerList.addEventListener("click", function(ev) {
|
ownerList.addEventListener("click", function(ev) {
|
||||||
if (ev.target.classList.contains("remove_owner")) {
|
if (ev.target.closest(".remove_owner")) {
|
||||||
ev.target.parentElement.remove();
|
ev.target.closest(".owner_row").remove();
|
||||||
}
|
}
|
||||||
updateAddOwnerStyles();
|
updateAddOwnerStyles();
|
||||||
|
updateOwnersPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateOwnersPreview() {
|
||||||
|
let ownerEls = ownerList.querySelectorAll(".owner_row");
|
||||||
|
ownersPreviewContainer.innerHTML = "";
|
||||||
|
for (let i = 0; i < ownerEls.length; ++i) {
|
||||||
|
let avatarUrl = ownerEls[i].querySelector("img").src;
|
||||||
|
let name = ownerEls[i].querySelector("span").textContent;
|
||||||
|
let previewEl = ownerPreviewTemplate();
|
||||||
|
previewEl.avatar.src = avatarUrl;
|
||||||
|
previewEl.name.textContent = name;
|
||||||
|
ownersPreviewContainer.appendChild(previewEl.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOwnersPreview();
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
// Logo / header management //
|
// Logo / header management //
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|
|
@ -8,50 +8,34 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
"git.handmade.network/hmn/hmn/src/utils"
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func APICheckUsername(c *RequestContext) ResponseData {
|
func APICheckUsername(c *RequestContext) ResponseData {
|
||||||
c.Req.ParseForm()
|
c.Req.ParseForm()
|
||||||
usernameArgs, hasUsername := c.Req.Form["username"]
|
usernameArgs, hasUsername := c.Req.Form["username"]
|
||||||
found := false
|
var user *models.User
|
||||||
canonicalUsername := ""
|
var err error
|
||||||
if hasUsername {
|
if hasUsername {
|
||||||
requestedUsername := usernameArgs[0]
|
requestedUsername := usernameArgs[0]
|
||||||
found = true
|
user, err = hmndata.FetchUserByUsername(c, c.Conn, c.CurrentUser, requestedUsername, hmndata.UsersQuery{})
|
||||||
c.Perf.StartBlock("SQL", "Fetch user")
|
if err != nil && !errors.Is(err, db.NotFound) {
|
||||||
user, err := db.QueryOne[models.User](c, c.Conn,
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername))
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
hmn_user
|
|
||||||
WHERE
|
|
||||||
LOWER(hmn_user.username) = LOWER($1)
|
|
||||||
AND status = ANY ($2)
|
|
||||||
`,
|
|
||||||
requestedUsername,
|
|
||||||
[]models.UserStatus{models.UserStatusConfirmed, models.UserStatusApproved},
|
|
||||||
)
|
|
||||||
c.Perf.EndBlock()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, db.NotFound) {
|
|
||||||
found = false
|
|
||||||
} else {
|
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canonicalUsername = user.Username
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
addCORSHeaders(c, &res)
|
addCORSHeaders(c, &res)
|
||||||
if found {
|
if user != nil {
|
||||||
res.WriteJson(map[string]any{
|
res.WriteJson(map[string]any{
|
||||||
"found": true,
|
"found": true,
|
||||||
"canonical": canonicalUsername,
|
"username": user.Username,
|
||||||
|
"name": user.BestName(),
|
||||||
|
"avatarUrl": templates.UserAvatarUrl(user),
|
||||||
}, nil)
|
}, nil)
|
||||||
} else {
|
} else {
|
||||||
res.WriteJson(map[string]any{
|
res.WriteJson(map[string]any{
|
||||||
|
|
Loading…
Reference in New Issue