Add notification count to user, populate sidebar via alpine
This commit is contained in:
parent
6fb964852f
commit
3e1b56a266
11 changed files with 3126 additions and 54 deletions
|
|
@ -27,6 +27,7 @@ func AddRoutes(r chi.Router) {
|
|||
r.Method("GET", "/signal", authenticatedHandlerJSON(listSignal))
|
||||
r.Method("GET", "/trap-data", auth.NewEnsureAuth(apiTrapData))
|
||||
r.Method("GET", "/tile/{z}/{y}/{x}", auth.NewEnsureAuth(getTile))
|
||||
r.Method("GET", "/user", authenticatedHandlerJSON(getUser))
|
||||
|
||||
// Unauthenticated endpoints
|
||||
r.Get("/district", apiGetDistrict)
|
||||
|
|
|
|||
18
api/user.go
Normal file
18
api/user.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
)
|
||||
|
||||
func getUser(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*platform.User, *nhttp.ErrorWithStatus) {
|
||||
counts, err := platform.NotificationCountsForUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("get notifications: %w", err)
|
||||
}
|
||||
user.NotificationCounts = *counts
|
||||
return &user, nil
|
||||
}
|
||||
3018
html/static/js/alpine-3.15.8-min.js
vendored
Normal file
3018
html/static/js/alpine-3.15.8-min.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -46,7 +46,9 @@ window.SSEManager = (function () {
|
|||
handlers.forEach((handler) => {
|
||||
eventSource.addEventListener("message", (message) => {
|
||||
const data = JSON.parse(message.data);
|
||||
handler(data);
|
||||
if (eventType == "*" || eventType == data.type) {
|
||||
handler(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
type="text/javascript"
|
||||
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
|
||||
></script>
|
||||
<script src="/static/js/time-relative.js"></script>
|
||||
<script src="/static/js/map-multipoint.js"></script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{{ define "sync/component/sidebar.html" }}
|
||||
<div id="sidebar">
|
||||
<div id="sidebar" x-data="$store.user">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo-container">
|
||||
<img class="logo" src="/static/img/nidus-logo-256-transparent.png" />
|
||||
|
|
@ -41,18 +41,16 @@
|
|||
>
|
||||
<div class="menu-icon">{{ template "messaging.svg" }}</div>
|
||||
<span class="menu-text ms-2">Communication</span>
|
||||
{{ if gt (len .User.Notifications) 0 }}
|
||||
<span
|
||||
x-show="notification_counts.communication > 0"
|
||||
x-cloak
|
||||
class="position-absolute translate-middle badge rounded-pill bg-primary"
|
||||
>
|
||||
<span
|
||||
class="position-absolute translate-middle badge rounded-pill bg-primary"
|
||||
>
|
||||
{{ if gt (len .User.Notifications) 99 }}
|
||||
99+
|
||||
{{ else }}
|
||||
{{ len .User.Notifications }}
|
||||
{{ end }}
|
||||
<span class="visually-hidden">unread notifications</span>
|
||||
</span>
|
||||
{{ end }}
|
||||
x-text="notification_counts.communication > 99 ? '99+' : notification_counts.communication"
|
||||
></span>
|
||||
<span class="visually-hidden">unread notifications</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -11,15 +11,18 @@
|
|||
<!-- favicon -->
|
||||
<link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" />
|
||||
<script src="/static/js/events.js"></script>
|
||||
<script defer src="/static/js/alpine-3.15.8-min.js"></script>
|
||||
{{ block "extraheader" . }}{{ end }}
|
||||
<script>
|
||||
const USER = {{ .User.AsJSON|json }};
|
||||
SSEManager.subscribe("*", function (e) {
|
||||
if (e.type == "created") {
|
||||
console.log("created event", e);
|
||||
} else {
|
||||
console.log("other event", e);
|
||||
if (e.type == "created" && e.resource.startsWith("rmo:")) {
|
||||
updateUserState();
|
||||
}
|
||||
});
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.store("user", USER);
|
||||
})
|
||||
function restoreLocalStorage() {
|
||||
const expanded = localStorage.getItem("sidebar.expanded");
|
||||
if (expanded == "false") {
|
||||
|
|
@ -47,6 +50,11 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
async function updateUserState() {
|
||||
const response = await fetch("/api/user");
|
||||
const data = await response.json();
|
||||
Alpine.store("user", data);
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
|
|
@ -67,8 +75,7 @@
|
|||
restoreLocalStorage();
|
||||
setTooltipsForSidebar();
|
||||
});
|
||||
</script>
|
||||
{{ if not .Config.IsProductionEnvironment }}
|
||||
</script> {{ if not .Config.IsProductionEnvironment }}
|
||||
<script src="/.flogo/injector.js"></script>
|
||||
{{ end }}
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
type="text/javascript"
|
||||
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
|
||||
></script>
|
||||
<script src="https://unpkg.com/@esri/maplibre-arcgis@1.1.0/dist/umd/maplibre-arcgis.min.js"></script>
|
||||
<script src="/static/js/map-arcgis-tile.js"></script>
|
||||
<script src="/static/js/map-multipoint.js"></script>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
type="text/javascript"
|
||||
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
|
||||
></script>
|
||||
<script
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
|
||||
></script>
|
||||
<script src="/static/js/map-proxied-arcgis-tile.js"></script>
|
||||
<script src="/static/js/map-multipoint.js"></script>
|
||||
<script src="/static/js/time-relative.js"></script>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ type Notification struct {
|
|||
Time time.Time
|
||||
Type string
|
||||
}
|
||||
type UserNotificationCounts struct {
|
||||
Communications uint `json:"communication"`
|
||||
Home uint `json:"home"`
|
||||
}
|
||||
|
||||
// Clear all notifications for a given user with the given path
|
||||
func ClearOauth(ctx context.Context, user *models.User) {
|
||||
|
|
@ -100,6 +104,26 @@ func NotificationsForUser(ctx context.Context, u User) ([]Notification, error) {
|
|||
}
|
||||
return results, nil
|
||||
}
|
||||
func NotificationCountsForUser(ctx context.Context, u User) (*UserNotificationCounts, error) {
|
||||
count_home, err := u.model.UserNotifications(
|
||||
models.SelectWhere.Notifications.ResolvedAt.IsNull(),
|
||||
).Count(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get home notification count: %w", err)
|
||||
}
|
||||
count_nuisance, err := u.Organization.model.Waters().Count(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get nuisance notification count: %w", err)
|
||||
}
|
||||
count_water, err := u.Organization.model.Waters().Count(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get water notification count: %w", err)
|
||||
}
|
||||
return &UserNotificationCounts{
|
||||
Communications: uint(count_nuisance + count_water),
|
||||
Home: uint(count_home),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func notificationTypeName(t enums.Notificationtype) string {
|
||||
switch t {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package platform
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -21,36 +22,51 @@ type NoUserError struct{}
|
|||
func (e NoUserError) Error() string { return "That user does not exist" }
|
||||
|
||||
type User struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
ID int `json:"-"`
|
||||
Initials string `json:"initials"`
|
||||
Notifications []Notification `json:"-"`
|
||||
Organization Organization `json:"organization"`
|
||||
PasswordHash string `json:"-"`
|
||||
PasswordHashType string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ID int `json:"-"`
|
||||
Initials string `json:"initials"`
|
||||
Notifications []Notification `json:"notifications"`
|
||||
NotificationCounts UserNotificationCounts `json:"notification_counts"`
|
||||
Organization Organization `json:"organization"`
|
||||
PasswordHash string `json:"-"`
|
||||
PasswordHashType string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
Username string `json:"username"`
|
||||
|
||||
model *models.User
|
||||
}
|
||||
|
||||
func (u User) AsJSON() string {
|
||||
content, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("{error: \"%s\"}", err.Error())
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
func (u User) HasRoot() bool {
|
||||
return u.model.Role == enums.UserroleRoot
|
||||
}
|
||||
func newUser(org Organization, user *models.User) User {
|
||||
return User{
|
||||
DisplayName: user.DisplayName,
|
||||
ID: int(user.ID),
|
||||
Initials: extractInitials(user.DisplayName),
|
||||
Notifications: []Notification{},
|
||||
Organization: org,
|
||||
PasswordHash: user.PasswordHash,
|
||||
PasswordHashType: string(user.PasswordHashType),
|
||||
Role: user.Role.String(),
|
||||
Username: user.Username,
|
||||
func newUser(ctx context.Context, org Organization, user *models.User) User {
|
||||
u := User{
|
||||
DisplayName: user.DisplayName,
|
||||
ID: int(user.ID),
|
||||
Initials: extractInitials(user.DisplayName),
|
||||
Notifications: []Notification{},
|
||||
NotificationCounts: UserNotificationCounts{},
|
||||
Organization: org,
|
||||
PasswordHash: user.PasswordHash,
|
||||
PasswordHashType: string(user.PasswordHashType),
|
||||
Role: user.Role.String(),
|
||||
Username: user.Username,
|
||||
|
||||
model: user,
|
||||
}
|
||||
counts, err := NotificationCountsForUser(ctx, u)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Int32("id", user.ID).Msg("failed to get notification counts for user")
|
||||
}
|
||||
u.NotificationCounts = *counts
|
||||
return u
|
||||
}
|
||||
|
||||
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
||||
|
|
@ -75,7 +91,7 @@ func CreateUser(ctx context.Context, username string, name string, password_hash
|
|||
return nil, fmt.Errorf("Failed to create user: %w", err)
|
||||
}
|
||||
log.Info().Int32("id", user.ID).Str("username", user.Username).Msg("Created user")
|
||||
u := newUser(newOrganization(o), user)
|
||||
u := newUser(ctx, newOrganization(o), user)
|
||||
return &u, nil
|
||||
}
|
||||
func UserByID(ctx context.Context, user_id int32) (*User, error) {
|
||||
|
|
@ -91,7 +107,7 @@ func UsersByOrg(ctx context.Context, org Organization) (map[int32]*User, error)
|
|||
}
|
||||
results := make(map[int32]*User, len(users))
|
||||
for _, user := range users {
|
||||
u := newUser(org, user)
|
||||
u := newUser(ctx, org, user)
|
||||
results[user.ID] = &u
|
||||
}
|
||||
return results, nil
|
||||
|
|
@ -113,7 +129,7 @@ func getUser(ctx context.Context, where mods.Where[*dialect.SelectQuery]) (*User
|
|||
}
|
||||
org := newOrganization(user.R.Organization)
|
||||
|
||||
u := newUser(org, user)
|
||||
u := newUser(ctx, org, user)
|
||||
return &u, nil
|
||||
}
|
||||
func extractInitials(name string) string {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue