Move users, calendars, and address books from config.yaml into the database
BREAKING CHANGE: the users:/config-based collection setup is gone. All
user, calendar, and address-book data now lives in the SQLite DB
(internal/db) and is managed exclusively via nidusctl or the web UI.
Existing deployments must recreate their users after upgrading:
nidusctl user create <username>
nidusctl calendar create <username> <name>
nidusctl addressbook create <username> <name>
- internal/db: new users, calendars, addressbooks tables with FK cascade
delete; foreign_keys pragma enabled; internal/db/users.go implements
full CRUD + bcrypt auth (CreateUser, VerifyPassword, ListUsers,
CreateCalendar/AddressBook, etc).
- internal/config: removed Users/UserConfig entirely.
- internal/auth: Basic Auth now checks credentials via db.DB instead of
cfg.Users.
- internal/caldav, internal/carddav: ListCalendars/ListAddressBooks and
Create/Delete now backed by the DB.
- internal/web: login uses db.VerifyPassword; new resources.go adds
create/delete handlers for calendars/address books at
/web/resources/{calendar,addressbook}; dashboard gained create forms
and per-card delete buttons (templ + htmx, no hyperscript).
- tools/nidusctl: new user create/delete/list/passwd commands (masked
interactive password prompt via golang.org/x/term) plus create/delete/
list subcommands for calendar/addressbook.
- cmd/server/main.go: pre-creates on-disk collections from the DB at
startup instead of cfg.Users; warns when no users exist yet.
- Updated tests to seed data via the DB; added resources_test.go for the
new web UI handlers.
- README.md and .github/copilot-instructions.md updated to document the
new nidusctl commands and the DB-backed architecture.
Verified end-to-end against a live test server: nidusctl user/calendar/
addressbook create, DAV Basic Auth PROPFIND, web login, dashboard
rendering, and web UI create/delete of resources all confirmed working.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+74
-63
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/yourusername/caldav-server/internal/db"
|
||||
@@ -10,83 +11,93 @@ import (
|
||||
|
||||
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
username := userFromContext(r.Context())
|
||||
user, ok := s.cfg.Users[username]
|
||||
if !ok {
|
||||
http.Error(w, "user not found in configuration", http.StatusInternalServerError)
|
||||
|
||||
resources, err := s.resourceCards(username)
|
||||
if err != nil {
|
||||
s.logger.Error("listing resources", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var resources []templates.ResourceCard
|
||||
|
||||
for _, calName := range user.Calendars {
|
||||
card := templates.ResourceCard{Kind: "calendar", Name: calName}
|
||||
if s.dbase != nil {
|
||||
shares, err := s.dbase.SharesOfCalendar(username, calName)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing calendar shares", "error", err)
|
||||
}
|
||||
for _, sh := range shares {
|
||||
card.Shares = append(card.Shares, templates.ShareRow{
|
||||
ResourceName: calName,
|
||||
SharedWith: sh.SharedWith,
|
||||
Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
}
|
||||
resources = append(resources, card)
|
||||
}
|
||||
|
||||
for _, bookName := range user.AddressBooks {
|
||||
card := templates.ResourceCard{Kind: "addressbook", Name: bookName}
|
||||
if s.dbase != nil {
|
||||
shares, err := s.dbase.SharesOfAddressBook(username, bookName)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing address book shares", "error", err)
|
||||
}
|
||||
for _, sh := range shares {
|
||||
card.Shares = append(card.Shares, templates.ShareRow{
|
||||
ResourceName: bookName,
|
||||
SharedWith: sh.SharedWith,
|
||||
Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
}
|
||||
resources = append(resources, card)
|
||||
}
|
||||
|
||||
var sharedWithMe []templates.SharedWithMeItem
|
||||
if s.dbase != nil {
|
||||
calShares, err := s.dbase.CalendarsSharedWith(username)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing calendars shared with user", "error", err)
|
||||
}
|
||||
for _, sh := range calShares {
|
||||
sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{
|
||||
Kind: "calendar", Owner: sh.Owner, Name: sh.CalendarName, Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
bookShares, err := s.dbase.AddressBooksSharedWith(username)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing address books shared with user", "error", err)
|
||||
}
|
||||
for _, sh := range bookShares {
|
||||
sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{
|
||||
Kind: "addressbook", Owner: sh.Owner, Name: sh.AddressBookName, Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
calShares, err := s.dbase.CalendarsSharedWith(username)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing calendars shared with user", "error", err)
|
||||
}
|
||||
for _, sh := range calShares {
|
||||
sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{
|
||||
Kind: "calendar", Owner: sh.Owner, Name: sh.CalendarName, Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
bookShares, err := s.dbase.AddressBooksSharedWith(username)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing address books shared with user", "error", err)
|
||||
}
|
||||
for _, sh := range bookShares {
|
||||
sharedWithMe = append(sharedWithMe, templates.SharedWithMeItem{
|
||||
Kind: "addressbook", Owner: sh.Owner, Name: sh.AddressBookName, Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.Dashboard(username, resources, sharedWithMe).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
// resourceCards builds the full list of ResourceCards (calendars, then
|
||||
// address books) owned by username, each with its current shares — used
|
||||
// both for the initial dashboard render and to re-render the whole
|
||||
// #resources list after a create/delete (since the set of cards changes,
|
||||
// unlike a share update which only changes one card's contents).
|
||||
func (s *Server) resourceCards(username string) ([]templates.ResourceCard, error) {
|
||||
var resources []templates.ResourceCard
|
||||
|
||||
calNames, err := s.dbase.ListCalendars(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing calendars: %w", err)
|
||||
}
|
||||
for _, calName := range calNames {
|
||||
card := templates.ResourceCard{Kind: "calendar", Name: calName}
|
||||
shares, err := s.dbase.SharesOfCalendar(username, calName)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing calendar shares", "error", err)
|
||||
}
|
||||
for _, sh := range shares {
|
||||
card.Shares = append(card.Shares, templates.ShareRow{
|
||||
ResourceName: calName,
|
||||
SharedWith: sh.SharedWith,
|
||||
Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
resources = append(resources, card)
|
||||
}
|
||||
|
||||
bookNames, err := s.dbase.ListAddressBooks(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing address books: %w", err)
|
||||
}
|
||||
for _, bookName := range bookNames {
|
||||
card := templates.ResourceCard{Kind: "addressbook", Name: bookName}
|
||||
shares, err := s.dbase.SharesOfAddressBook(username, bookName)
|
||||
if err != nil {
|
||||
s.logger.Warn("listing address book shares", "error", err)
|
||||
}
|
||||
for _, sh := range shares {
|
||||
card.Shares = append(card.Shares, templates.ShareRow{
|
||||
ResourceName: bookName,
|
||||
SharedWith: sh.SharedWith,
|
||||
Permission: string(sh.Permission),
|
||||
})
|
||||
}
|
||||
resources = append(resources, card)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// resourceCardFor rebuilds a single ResourceCard (used to re-render just
|
||||
// the card an htmx request just changed, for partial updates).
|
||||
func (s *Server) resourceCardFor(username, kind, name string) (templates.ResourceCard, error) {
|
||||
card := templates.ResourceCard{Kind: kind, Name: name}
|
||||
if s.dbase == nil {
|
||||
return card, nil
|
||||
}
|
||||
|
||||
var shares []templates.ShareRow
|
||||
if kind == "calendar" {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yourusername/caldav-server/internal/db"
|
||||
"github.com/yourusername/caldav-server/internal/web/templates"
|
||||
)
|
||||
|
||||
// resourceNameRe restricts calendar/address book names to characters that
|
||||
// are safe as both a URL path segment and a filesystem directory name.
|
||||
var resourceNameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
|
||||
|
||||
// handleCalendarResource handles POST (create) and DELETE (remove) for
|
||||
// the current user's own calendars, mounted at /resources/calendar.
|
||||
func (s *Server) handleCalendarResource(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleResource(w, r, "calendar")
|
||||
}
|
||||
|
||||
func (s *Server) handleAddressBookResource(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleResource(w, r, "addressbook")
|
||||
}
|
||||
|
||||
func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind string) {
|
||||
username := userFromContext(r.Context())
|
||||
|
||||
// htmx v2 sends DELETE request parameters as URL query parameters, not
|
||||
// a request body — unlike POST/PUT/PATCH (see internal/web/shares.go).
|
||||
if r.Method == http.MethodDelete {
|
||||
r.PostForm = r.URL.Query()
|
||||
} else if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(r.PostForm.Get("name"))
|
||||
if !resourceNameRe.MatchString(name) {
|
||||
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
collPrefix := "cal-"
|
||||
if kind == "addressbook" {
|
||||
collPrefix = "card-"
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
var err error
|
||||
if kind == "calendar" {
|
||||
err = s.dbase.CreateCalendar(username, name)
|
||||
} else {
|
||||
err = s.dbase.CreateAddressBook(username, name)
|
||||
}
|
||||
if err != nil {
|
||||
if err == db.ErrResourceExists {
|
||||
http.Error(w, "already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
s.logger.Error("creating resource", "kind", kind, "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := s.store.EnsureCollection(username, collPrefix+name); err != nil {
|
||||
s.logger.Error("creating resource storage", "kind", kind, "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case http.MethodDelete:
|
||||
var err error
|
||||
if kind == "calendar" {
|
||||
err = s.dbase.DeleteCalendar(username, name)
|
||||
} else {
|
||||
err = s.dbase.DeleteAddressBook(username, name)
|
||||
}
|
||||
if err != nil && err != db.ErrResourceNotFound {
|
||||
s.logger.Error("deleting resource", "kind", kind, "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteCollection(username, collPrefix+name); err != nil {
|
||||
s.logger.Warn("deleting resource storage", "kind", kind, "error", err)
|
||||
}
|
||||
default:
|
||||
w.Header().Set("Allow", "POST, DELETE")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// The set of cards changed (one added/removed), so re-render the
|
||||
// whole #resources list rather than a single card.
|
||||
resources, err := s.resourceCards(username)
|
||||
if err != nil {
|
||||
s.logger.Error("listing resources", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.ResourceList(resources).Render(context.Background(), w)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateAndDeleteCalendarViaWebUI(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
handler := s.Handler(emptyStaticFS{})
|
||||
cookie := loginAs(t, handler, "alice", "password")
|
||||
|
||||
// Create a new calendar.
|
||||
form := url.Values{"name": {"vacation"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/resources/calendar", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(cookie)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("create: expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "vacation") {
|
||||
t.Fatalf("expected resource list to include new calendar, got: %s", rr.Body.String())
|
||||
}
|
||||
|
||||
names, err := s.dbase.ListCalendars("alice")
|
||||
if err != nil {
|
||||
t.Fatalf("ListCalendars: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range names {
|
||||
if n == "vacation" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected vacation calendar to be registered, got %v", names)
|
||||
}
|
||||
|
||||
// Duplicate creation should fail with 409.
|
||||
req = httptest.NewRequest(http.MethodPost, "/resources/calendar", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(cookie)
|
||||
rr = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusConflict {
|
||||
t.Fatalf("expected 409 on duplicate create, got %d", rr.Code)
|
||||
}
|
||||
|
||||
// Delete it — htmx v2 sends DELETE params as a URL query string.
|
||||
req = httptest.NewRequest(http.MethodDelete, "/resources/calendar?name=vacation", nil)
|
||||
req.AddCookie(cookie)
|
||||
rr = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("delete: expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if strings.Contains(rr.Body.String(), "vacation") {
|
||||
t.Fatalf("expected resource list to no longer include deleted calendar, got: %s", rr.Body.String())
|
||||
}
|
||||
|
||||
names, err = s.dbase.ListCalendars("alice")
|
||||
if err != nil {
|
||||
t.Fatalf("ListCalendars: %v", err)
|
||||
}
|
||||
for _, n := range names {
|
||||
if n == "vacation" {
|
||||
t.Fatalf("expected vacation calendar to be gone, got %v", names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAddressBookInvalidName(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
handler := s.Handler(emptyStaticFS{})
|
||||
cookie := loginAs(t, handler, "alice", "password")
|
||||
|
||||
form := url.Values{"name": {"has a space"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/resources/addressbook", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(cookie)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400 for invalid name, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCalendarRequiresLogin(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
handler := s.Handler(emptyStaticFS{})
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/resources/calendar?name=work", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusSeeOther {
|
||||
t.Fatalf("expected redirect to login, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/yourusername/caldav-server/internal/config"
|
||||
"github.com/yourusername/caldav-server/internal/db"
|
||||
"github.com/yourusername/caldav-server/internal/store"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Server holds the dependencies needed by the web UI handlers.
|
||||
@@ -43,19 +42,16 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
|
||||
mux.HandleFunc("/", s.requireLogin(s.handleDashboard))
|
||||
mux.HandleFunc("/shares/calendar", s.requireLogin(s.handleCalendarShare))
|
||||
mux.HandleFunc("/shares/addressbook", s.requireLogin(s.handleAddressBookShare))
|
||||
mux.HandleFunc("/resources/calendar", s.requireLogin(s.handleCalendarResource))
|
||||
mux.HandleFunc("/resources/addressbook", s.requireLogin(s.handleAddressBookResource))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// authenticate validates username/password against the configured users,
|
||||
// mirroring internal/auth's Basic Auth check.
|
||||
// authenticate validates username/password against the database, mirroring
|
||||
// internal/auth's Basic Auth check.
|
||||
func (s *Server) authenticate(username, password string) bool {
|
||||
user, ok := s.cfg.Users[username]
|
||||
if !ok {
|
||||
_ = bcrypt.CompareHashAndPassword([]byte("$2b$12$invalidinvalidinvalidinvalidinvalidinval"), []byte(password))
|
||||
return false
|
||||
}
|
||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||
return s.dbase.VerifyPassword(username, password)
|
||||
}
|
||||
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+14
-10
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/yourusername/caldav-server/internal/config"
|
||||
"github.com/yourusername/caldav-server/internal/db"
|
||||
"github.com/yourusername/caldav-server/internal/store"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func newTestServer(t *testing.T) *Server {
|
||||
@@ -31,16 +30,21 @@ func newTestServer(t *testing.T) *Server {
|
||||
}
|
||||
t.Cleanup(func() { dbase.Close() })
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateFromPassword: %v", err)
|
||||
cfg := &config.Config{}
|
||||
if err := dbase.CreateUser("alice", "password", "", ""); err != nil {
|
||||
t.Fatalf("CreateUser alice: %v", err)
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
Users: map[string]config.UserConfig{
|
||||
"alice": {Password: string(hash), Calendars: []string{"work"}, AddressBooks: []string{"contacts"}},
|
||||
"bob": {Password: string(hash), Calendars: []string{"personal"}},
|
||||
},
|
||||
if err := dbase.CreateUser("bob", "password", "", ""); err != nil {
|
||||
t.Fatalf("CreateUser bob: %v", err)
|
||||
}
|
||||
if err := dbase.CreateCalendar("alice", "work"); err != nil {
|
||||
t.Fatalf("CreateCalendar alice/work: %v", err)
|
||||
}
|
||||
if err := dbase.CreateAddressBook("alice", "contacts"); err != nil {
|
||||
t.Fatalf("CreateAddressBook alice/contacts: %v", err)
|
||||
}
|
||||
if err := dbase.CreateCalendar("bob", "personal"); err != nil {
|
||||
t.Fatalf("CreateCalendar bob/personal: %v", err)
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
return NewServer(cfg, st, dbase, logger)
|
||||
|
||||
+10
-12
@@ -24,11 +24,6 @@ func (s *Server) handleAddressBookShare(w http.ResponseWriter, r *http.Request)
|
||||
func (s *Server) handleShare(w http.ResponseWriter, r *http.Request, kind string) {
|
||||
username := userFromContext(r.Context())
|
||||
|
||||
if s.dbase == nil {
|
||||
http.Error(w, "sharing is not available (no database configured)", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// htmx v2 sends DELETE request parameters (including hx-vals) as URL
|
||||
// query parameters, not a request body — unlike POST/PUT/PATCH.
|
||||
if r.Method == http.MethodDelete {
|
||||
@@ -100,15 +95,18 @@ func (s *Server) handleShare(w http.ResponseWriter, r *http.Request, kind string
|
||||
// actually configured for username, to prevent sharing arbitrary/other
|
||||
// users' resources via a forged form post.
|
||||
func (s *Server) ownsResource(username, kind, resource string) bool {
|
||||
user, ok := s.cfg.Users[username]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
var list []string
|
||||
var (
|
||||
list []string
|
||||
err error
|
||||
)
|
||||
if kind == "calendar" {
|
||||
list = user.Calendars
|
||||
list, err = s.dbase.ListCalendars(username)
|
||||
} else {
|
||||
list = user.AddressBooks
|
||||
list, err = s.dbase.ListAddressBooks(username)
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Warn("checking resource ownership", "kind", kind, "error", err)
|
||||
return false
|
||||
}
|
||||
for _, n := range list {
|
||||
if n == resource {
|
||||
|
||||
@@ -28,12 +28,48 @@ type SharedWithMeItem struct {
|
||||
templ Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedWithMeItem) {
|
||||
@Layout("Dashboard", username) {
|
||||
<h1 class="text-2xl font-semibold mb-6">Your calendars & address books</h1>
|
||||
<div id="resources" class="space-y-6">
|
||||
for _, r := range resources {
|
||||
@ResourceCardView(r)
|
||||
}
|
||||
|
||||
<div class="flex gap-4 mb-6">
|
||||
<form
|
||||
class="flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
|
||||
hx-post="/web/resources/calendar"
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">New calendar</label>
|
||||
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
|
||||
placeholder="e.g. work"
|
||||
class="rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
<form
|
||||
class="flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
|
||||
hx-post="/web/resources/addressbook"
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">New address book</label>
|
||||
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
|
||||
placeholder="e.g. contacts"
|
||||
class="rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ResourceList(resources)
|
||||
|
||||
if len(sharedWithMe) > 0 {
|
||||
<h2 class="text-xl font-semibold mt-10 mb-4">Shared with you</h2>
|
||||
<ul class="divide-y divide-gray-200 bg-white rounded-lg border border-gray-200">
|
||||
@@ -51,6 +87,22 @@ templ Dashboard(username string, resources []ResourceCard, sharedWithMe []Shared
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceList renders the #resources container. It's re-rendered as a
|
||||
// whole after a create/delete (which changes the set of cards), whereas a
|
||||
// share update only swaps a single ResourceCardView.
|
||||
templ ResourceList(resources []ResourceCard) {
|
||||
<div id="resources" class="space-y-6">
|
||||
for _, r := range resources {
|
||||
@ResourceCardView(r)
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
<p class="text-sm text-gray-400">
|
||||
You don't have any calendars or address books yet — add one above.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ResourceCardView(r ResourceCard) {
|
||||
<div id={ "resource-" + r.Kind + "-" + r.Name } class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -58,6 +110,16 @@ templ ResourceCardView(r ResourceCard) {
|
||||
{ r.Name }
|
||||
<span class="text-xs uppercase tracking-wide text-gray-400 ml-2">{ r.Kind }</span>
|
||||
</h2>
|
||||
<button
|
||||
class="text-red-600 hover:underline text-xs"
|
||||
hx-delete={ resourceEndpoint(r.Kind) }
|
||||
hx-vals={ resourceVals(r.Name) }
|
||||
hx-target="#resources"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm={ "Delete " + r.Kind + " " + r.Name + "? This removes all its data and cannot be undone." }
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-gray-100 mb-4">
|
||||
@@ -121,3 +183,15 @@ func shareEndpoint(kind string) string {
|
||||
func shareVals(resource, sharedWith string) string {
|
||||
return `{"resource": "` + resource + `", "shared_with": "` + sharedWith + `"}`
|
||||
}
|
||||
|
||||
func resourceEndpoint(kind string) string {
|
||||
if kind == "calendar" {
|
||||
return "/web/resources/calendar"
|
||||
}
|
||||
return "/web/resources/addressbook"
|
||||
}
|
||||
|
||||
func resourceVals(name string) string {
|
||||
return `{"name": "` + name + `"}`
|
||||
}
|
||||
|
||||
|
||||
@@ -66,17 +66,15 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Your calendars & address books</h1><div id=\"resources\" class=\"space-y-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Your calendars & address books</h1><div class=\"flex gap-4 mb-6\"><form class=\"flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/calendar\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div><label class=\"block text-xs text-gray-500 mb-1\">New calendar</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. work\" class=\"rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form><form class=\"flex items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/addressbook\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div><label class=\"block text-xs text-gray-500 mb-1\">New address book</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. contacts\" class=\"rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, r := range resources {
|
||||
templ_7745c5c3_Err = ResourceCardView(r).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ResourceList(resources).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -93,7 +91,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Owner)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 43, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 79, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -106,7 +104,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 43, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 79, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -119,7 +117,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Kind)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 44, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 80, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -132,7 +130,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Permission)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 46, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 82, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -158,7 +156,10 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
|
||||
})
|
||||
}
|
||||
|
||||
func ResourceCardView(r ResourceCard) templ.Component {
|
||||
// ResourceList renders the #resources container. It's re-rendered as a
|
||||
// whole after a create/delete (which changes the set of cards), whereas a
|
||||
// share update only swaps a single ResourceCardView.
|
||||
func ResourceList(resources []ResourceCard) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -179,179 +180,263 @@ func ResourceCardView(r ResourceCard) templ.Component {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div id=\"resources\" class=\"space-y-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("resource-" + r.Kind + "-" + r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 55, Col: 46}
|
||||
for _, r := range resources {
|
||||
templ_7745c5c3_Err = ResourceCardView(r).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if len(resources) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"text-sm text-gray-400\">You don't have any calendars or address books yet — add one above.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" class=\"bg-white rounded-lg border border-gray-200 p-5\"><div class=\"flex items-center justify-between mb-3\"><h2 class=\"font-medium\">")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ResourceCardView(r ResourceCard) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("resource-" + r.Kind + "-" + r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 58, Col: 12}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 107, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " <span class=\"text-xs uppercase tracking-wide text-gray-400 ml-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"bg-white rounded-lg border border-gray-200 p-5\"><div class=\"flex items-center justify-between mb-3\"><h2 class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Kind)
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 59, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 110, Col: 12}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></h2></div><ul class=\"divide-y divide-gray-100 mb-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " <span class=\"text-xs uppercase tracking-wide text-gray-400 ml-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Kind)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 111, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></h2><button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(resourceEndpoint(r.Kind))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 115, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-vals=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(resourceVals(r.Name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 116, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-confirm=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + r.Kind + " " + r.Name + "? This removes all its data and cannot be undone.")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 119, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">Delete</button></div><ul class=\"divide-y divide-gray-100 mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, sh := range r.Shares {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<li class=\"py-2 flex items-center justify-between text-sm\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(sh.SharedWith)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 66, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span> <span class=\"flex items-center gap-3\"><span class=\"text-xs uppercase tracking-wide text-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(sh.Permission)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 68, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> <button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 71, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-vals=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareVals(r.Name, sh.SharedWith))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 72, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<li class=\"py-2 flex items-center justify-between text-sm\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(sh.SharedWith)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 73, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 128, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-swap=\"outerHTML\" hx-confirm=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</span> <span class=\"flex items-center gap-3\"><span class=\"text-xs uppercase tracking-wide text-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove access for " + sh.SharedWith + "?")
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(sh.Permission)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 75, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 130, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">Remove</button></span></li>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</span> <button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 133, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" hx-vals=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareVals(r.Name, sh.SharedWith))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 134, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 135, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" hx-swap=\"outerHTML\" hx-confirm=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove access for " + sh.SharedWith + "?")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 137, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">Remove</button></span></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(r.Shares) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<li class=\"py-2 text-sm text-gray-400\">Not shared with anyone yet.</li>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<li class=\"py-2 text-sm text-gray-400\">Not shared with anyone yet.</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</ul><form class=\"flex items-end gap-2\" hx-post=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</ul><form class=\"flex items-end gap-2\" hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 89, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 151, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 90, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 152, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"resource\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"resource\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name)
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 93, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 155, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">Username</label> <input name=\"shared_with\" type=\"text\" required class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Permission</label> <select name=\"permission\" class=\"rounded-md border-gray-300 border px-2 py-1.5 text-sm\"><option value=\"read\">read</option> <option value=\"write\">write</option></select></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Share</button></form></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">Username</label> <input name=\"shared_with\" type=\"text\" required class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Permission</label> <select name=\"permission\" class=\"rounded-md border-gray-300 border px-2 py-1.5 text-sm\"><option value=\"read\">read</option> <option value=\"write\">write</option></select></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Share</button></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -370,4 +455,15 @@ func shareVals(resource, sharedWith string) string {
|
||||
return `{"resource": "` + resource + `", "shared_with": "` + sharedWith + `"}`
|
||||
}
|
||||
|
||||
func resourceEndpoint(kind string) string {
|
||||
if kind == "calendar" {
|
||||
return "/web/resources/calendar"
|
||||
}
|
||||
return "/web/resources/addressbook"
|
||||
}
|
||||
|
||||
func resourceVals(name string) string {
|
||||
return `{"name": "` + name + `"}`
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
|
||||
Reference in New Issue
Block a user