Add conflict resolution diff UI and app branding
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/tim/md-hub-secure/apps/server/internal/docs"
|
||||
"github.com/tim/cairnquire/apps/server/internal/docs"
|
||||
)
|
||||
|
||||
type layoutData struct {
|
||||
@@ -42,6 +43,16 @@ type documentData struct {
|
||||
Breadcrumbs []breadcrumb
|
||||
}
|
||||
|
||||
type documentEditData struct {
|
||||
Browser browserData
|
||||
Title string
|
||||
Path string
|
||||
Tags []string
|
||||
Hash string
|
||||
Source string
|
||||
Breadcrumbs []breadcrumb
|
||||
}
|
||||
|
||||
type breadcrumb struct {
|
||||
Name string
|
||||
URL string
|
||||
@@ -86,7 +97,7 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
activeFolder := strings.Trim(r.URL.Query().Get("folder"), "/")
|
||||
s.renderTemplate(w, http.StatusOK, "index.gohtml", layoutData{
|
||||
Title: "MD Hub Secure",
|
||||
Title: "Cairnquire",
|
||||
WebEnabled: s.webEnabled,
|
||||
BodyClass: "page-index",
|
||||
BodyTemplate: "index_content",
|
||||
@@ -126,9 +137,47 @@ func (s *Server) handleDocsIndex(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleDocument(w http.ResponseWriter, r *http.Request) {
|
||||
pagePath := chi.URLParam(r, "*")
|
||||
if editPath, ok := trimEditSuffix(pagePath); ok {
|
||||
s.renderDocumentEditor(w, r, editPath)
|
||||
return
|
||||
}
|
||||
s.renderDocumentPage(w, r, pagePath)
|
||||
}
|
||||
|
||||
func (s *Server) renderDocumentEditor(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
page, err := s.documents.LoadSourcePage(r.Context(), pagePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
s.renderError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
s.renderError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items, err := s.documents.ListDocuments(r.Context())
|
||||
if err != nil {
|
||||
s.renderError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.renderTemplate(w, http.StatusOK, "document_edit.gohtml", layoutData{
|
||||
Title: "Edit: " + page.Title,
|
||||
WebEnabled: s.webEnabled,
|
||||
BodyClass: "page-document-edit",
|
||||
BodyTemplate: "document_edit_content",
|
||||
Data: documentEditData{
|
||||
Browser: buildBrowser(items, page.Path),
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
Tags: page.Tags,
|
||||
Hash: page.Hash,
|
||||
Source: page.Content,
|
||||
Breadcrumbs: buildBreadcrumbs(page.Path),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) renderDocumentPage(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
page, err := s.documents.LoadPage(r.Context(), pagePath)
|
||||
if err != nil {
|
||||
@@ -184,6 +233,20 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleServiceWorker(w http.ResponseWriter, r *http.Request) {
|
||||
content, err := fs.ReadFile(assets, "static/sw.js")
|
||||
if err != nil {
|
||||
http.Error(w, "service worker unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Service-Worker-Allowed", "/")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(content)
|
||||
}
|
||||
|
||||
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
@@ -340,6 +403,52 @@ func (s *Server) handleDocuments(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"documents": results})
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentSave(w http.ResponseWriter, r *http.Request) {
|
||||
pagePath := chi.URLParam(r, "*")
|
||||
if pagePath == "" {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document path is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
BaseHash string `json:"baseHash"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
page, err := s.documents.SaveSourcePageWithBaseHash(r.Context(), pagePath, req.Content, req.BaseHash)
|
||||
if err != nil {
|
||||
var conflict *docs.DocumentConflictError
|
||||
if errors.As(err, &conflict) {
|
||||
writeJSONWithStatus(w, http.StatusConflict, map[string]any{
|
||||
"error": "document conflict",
|
||||
"status": "conflict",
|
||||
"path": conflict.Path,
|
||||
"baseHash": conflict.BaseHash,
|
||||
"currentHash": conflict.CurrentHash,
|
||||
"currentContent": conflict.CurrentContent,
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONWithStatus(w, http.StatusOK, map[string]any{
|
||||
"status": "saved",
|
||||
"path": page.Path,
|
||||
"title": page.Title,
|
||||
"hash": page.Hash,
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
writeJSONWithStatus(w, status, payload)
|
||||
}
|
||||
@@ -350,6 +459,16 @@ func writeJSONWithStatus(w http.ResponseWriter, status int, payload any) {
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func trimEditSuffix(path string) (string, bool) {
|
||||
if path == "edit" {
|
||||
return "", true
|
||||
}
|
||||
if !strings.HasSuffix(path, "/edit") {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSuffix(path, "/edit"), true
|
||||
}
|
||||
|
||||
func buildBrowser(records []docs.DocumentRecord, activePath string) browserData {
|
||||
paths := make([]string, 0, len(records))
|
||||
titleByPath := make(map[string]string, len(records))
|
||||
|
||||
Reference in New Issue
Block a user