account page: fixed platform reordering bugs

This commit is contained in:
2026-01-28 02:14:27 -07:00
parent a50c0ef18d
commit 8f38f141e5
8 changed files with 141 additions and 72 deletions
+19 -9
View File
@@ -12,10 +12,15 @@ import (
)
// TODO: simpify, if possible (single query ideal)
func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int64, platform Platform, orderIndex int) error {
func (db *Store) SetOrderOfPlatformOnAccountPage(
ctx context.Context,
acctID int64,
platform Platform,
orderIndex int,
) (orderedPlatforms []Platform, prevOrderIndex int, err error) {
tx, err := db.db.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
return nil, 0, fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback(ctx)
@@ -39,7 +44,7 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
},
)
if err != nil {
return fmt.Errorf("failed to perform query to look up existing indexes: %w", err)
return nil, 0, fmt.Errorf("failed to perform query to look up existing indexes: %w", err)
}
indexes, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
@@ -47,7 +52,7 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
Order_index int
}])
if err != nil {
return fmt.Errorf("failed to scan rows for query to look up existing indexes: %w", err)
return nil, 0, fmt.Errorf("failed to scan rows for query to look up existing indexes: %w", err)
}
// compute the implied indexes,
@@ -74,9 +79,13 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
}
}
prevOrderIndex := indexPerPlatform[platform]
prevOrderIndex = indexPerPlatform[platform]
orderedPlatforms = make([]Platform, len(allPlatforms))
if indexPerPlatform[platform] == orderIndex {
return nil
for i := range allPlatforms {
orderedPlatforms[i] = platformPerIndex[i]
}
return orderedPlatforms, prevOrderIndex, nil
}
if increased := orderIndex > prevOrderIndex; increased {
@@ -110,6 +119,7 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
valuesLines[i] = fmt.Sprintf("(@account_id, @platform_%d, @order_index_%d::smallint)", i, i)
args[fmt.Sprintf("platform_%d", i)] = p
args[fmt.Sprintf("order_index_%d", i)] = indexPerPlatform[p]
orderedPlatforms[i] = platformPerIndex[i]
}
_, err = tx.Exec(
@@ -148,14 +158,14 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
args,
)
if err != nil {
return fmt.Errorf("failed to perform query to delete old indexes and insert new indexes: %w", err)
return nil, 0, fmt.Errorf("failed to perform query to delete old indexes and insert new indexes: %w", err)
}
if tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit txn: %w", err)
return nil, 0, fmt.Errorf("failed to commit txn: %w", err)
}
return nil
return orderedPlatforms, prevOrderIndex, nil
}
func (db *Store) GetOrderOfPlatformsOnAccountPage(ctx context.Context, acctID int64) ([]Platform, error) {
@@ -51,7 +51,7 @@ func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Accoun
return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID)
}
func (v_ctx *StoreWithContext) SetOrderOfPlatformOnAccountPage(acctID int64, platform Platform, orderIndex int) error {
func (v_ctx *StoreWithContext) SetOrderOfPlatformOnAccountPage(acctID int64, platform Platform, orderIndex int) ([]Platform, int, error) {
return v_ctx.Store.SetOrderOfPlatformOnAccountPage(v_ctx.ctx, acctID, platform, orderIndex)
}
+34 -2
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -19,6 +20,7 @@ import (
type accountSubrouter struct {
log *logging.Logger
accts *accounts.Store
pub *sse.UpdateNotificationPublisher
}
func Routes(
@@ -30,6 +32,7 @@ func Routes(
as := &accountSubrouter{
log: logger,
accts: accts,
pub: pub,
}
r.POST("", response.Handler(as.createAccount))
@@ -187,9 +190,13 @@ func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error)
return orderIndex, nil
}
// PUT /:acctID/platforms/:platform/order-index"
// this endpoint is called when dragging a platform tab in the accounts page.
func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
// validate parameters
var orderIndex int
if v, ok := c.GetPostForm("order-index"); !ok {
return nil, response.BadRequest().
@@ -216,9 +223,34 @@ func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (resp
platform = p
}
if err := s.accts.SetOrderOfPlatformOnAccountPage(c, acctID, platform, orderIndex); err != nil {
// update the order
platforms, prevIndex, err := s.accts.SetOrderOfPlatformOnAccountPage(c, acctID, platform, orderIndex)
if err != nil {
return nil, fmt.Errorf("failed to save record: %w", err)
}
return response.Status(200), nil
// emit events on all platforms updated, so the tabs can all refresh (including their logic)
platformEvents := make([]string, max(orderIndex, prevIndex)-min(orderIndex, prevIndex))
if orderIndex > prevIndex {
for i := range orderIndex - prevIndex {
p := platforms[prevIndex+i]
platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p))
}
} else if orderIndex < prevIndex {
for i := range prevIndex - orderIndex {
p := platforms[orderIndex+1+i]
platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p))
}
}
if err := s.pub.Push(c, acctID, platformEvents...); err != nil {
s.log.Errorf("failed to publish platform order-index events: %v", err)
}
return response.StatusNoContent(), nil
}
func lowerSnakeCase(s accounts.Platform) string {
return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_"))
}
+30
View File
@@ -2,9 +2,11 @@ package sse
import (
"context"
"errors"
"fmt"
"path"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -120,6 +122,34 @@ func (p *UpdateNotificationPublisher) Publish(pathPattern string) gin.HandlerFun
}
}
func (p *UpdateNotificationPublisher) Push(ctx context.Context, acctID int64, eventTypes ...string) error {
var wg sync.WaitGroup
wg.Add(len(eventTypes))
errs := make([]error, len(eventTypes))
for i, e := range eventTypes {
i := i
e := e
go func() {
defer wg.Done()
if err := p.queue.Send(ctx, Event{
AccountID: acctID,
Type: e,
Data: []byte(fmt.Sprintf(`{"eventType": %q}`, e)),
}); err != nil {
errs[i] = fmt.Errorf("failed to send sse event to listener: %w", err)
}
}()
}
wg.Wait()
return errors.Join(errs...)
return nil
}
func getPathSegments(p string) []string {
p = path.Clean(p)
if p == "" || p == "." || p == "/" {
+1 -1
View File
@@ -66,7 +66,7 @@ func Routes(
},
// strings
"lowerCamelCase": func(s string) string {
"lowerSnakeCase": func(s string) string {
return strings.ToLower(strings.Join(strings.Split(s, " "), "_"))
},
-5
View File
@@ -114,9 +114,6 @@
const dragAttr = GetAttribute(drag, "hx-drag");
const dropAttr = GetAttribute(drop, "hx-drop");
console.log('dragAttr:', dragAttr);
console.log('dropAttr:', dropAttr);
const dragVals = JSON.parse(GetAttribute(drag, "hx-drag") || "{}");
const dropVals = JSON.parse(GetAttribute(drop, "hx-drop") || "{}");
@@ -159,8 +156,6 @@
values = Object.assign({}, dragVals, dropVals);
}
console.log('action:', action)
if (action === null) return;
try { // don't let one failure cascade to the other ajax
@@ -6,44 +6,48 @@
{{- end }}
{{- $platformSegment := lowerCamelCase (printf "%s" $platform) }}
{{- $path := printf "/accounts/%d/platforms/%s/order-index" $acctID $platformSegment }}
{{- $platformSegment := lowerSnakeCase (printf "%s" $platform) }}
{{- $updatePath := printf "/accounts/%d/platforms/%s/order-index" $acctID $platformSegment }}
<li
id="{{$platformSegment}}"
class="my-[1rem] rounded-lg bg-accent"
hx-drag-action="/api{{$path}}"
hx-drop='{"order-index": "{{$orderIndex}}"}'
{{/* the dragged <li> will be NOT be updated by the response (204 response), but instead the following sse events */}}
hx-drag="{}"
hx-trigger="sse:{{$path}}"
hx-get="/ui{{$path}}"
hx-on::sse-message="console.log('test')"
hx-on::sseMessage="console.log('test')"
hx-on::sseOpen="console.log('test')"
hx-on::sse-open="console.log('test')"
hx-on:htmx:sse-message="console.log('test')"
hx-on:htmx:sseMessage="console.log('test')"
hx-on:htmx:sseOpen="console.log('test')"
hx-on:htmx:sse-open="console.log('test')"
hx-on::htmx:sse-message="console.log('test')"
hx-on::htmx:sseMessage="console.log('test')"
hx-on::htmx:sseOpen="console.log('test')"
hx-on::htmx:sse-open="console.log('test')"
hx-on--sse-message="console.log('test')"
hx-on--sseMessage="console.log('test')"
hx-on--sseOpen="console.log('test')"
hx-on--sse-open="console.log('test')"
hx-on-htmx-sse-message="console.log('test')"
hx-on-htmx-sseMessage="console.log('test')"
hx-on-htmx-sseOpen="console.log('test')"
hx-on-htmx-sse-open="console.log('test')"
hx-on--htmx-sse-message="console.log('test')"
hx-on--htmx-sseMessage="console.log('test')"
hx-on--htmx-sseOpen="console.log('test')"
hx-on--htmx-sse-open="console.log('test')"
hx-drop='{"order-index": "{{$orderIndex}}"}'
hx-drag-action={{printf "/api/accounts/%d/platforms/%s/order-index" $acctID $platformSegment}}
hx-trigger={{printf "sse:accounts_%d_platforms_%s_order-index" $acctID $platformSegment }}
hx-get={{printf "/ui/accounts/%d/shops/list-items/%s" $acctID $platformSegment}}
draggable="true"
data-order-index={{$orderIndex}}
_="
on dragstart
set event.dataTransfer.effectAllowed to 'move'
event.dataTransfer.setData('platform-section', my id)
event.dataTransfer.setData('order-index', my dataset.orderIndex)
on dragover
if event.dataTransfer.types.includes('platform-section') then
event.preventDefault()
end
on drop
event.preventDefault()
set draggedID to event.dataTransfer.getData('platform-section')
set draggedOrderIndex to event.dataTransfer.getData('order-index')
set myOrderIndex to parseInt(my dataset.orderIndex)
set draggedSection to #{draggedID}
if draggedOrderIndex < myOrderIndex then
me.after(draggedSection)
else
me.before(draggedSection)
end
"
>
<details
open
id="{{$platformSegment}}-details"
class="
group
@@ -65,8 +69,10 @@
details-content:transition-[opacity_transform]
"
hx-preserve
>
<summary class="
<summary
class="
list-none
p-[1em]
after:content-['+']
@@ -76,7 +82,8 @@
after:text-[2em]
after:leading-[1em]
cursor-pointer
">
"
>
{{/*
group-open:border-b-background
group-open:border-b-[0.25rem]
@@ -85,12 +92,12 @@
<h3 class="inline-block">{{$platform.PrettyPrint}}</h3>
</summary>
<div class="p-[1em] text-center">
{{- $pathSegment := lowerCamelCase (printf "%s" $platform) }}
{{-
component
(printf "/accounts/%d/shops/link-sections/%s" $acctID $pathSegment)
}}
<div
id="{{$platform}}-tab"
class="p-[1em] text-center"
>
{{- $pathSegment := lowerSnakeCase (printf "%s" $platform) }}
{{- component (printf "/accounts/%d/shops/link-sections/%s" $acctID $pathSegment) }}
</div>
</details>
</li>
@@ -57,11 +57,6 @@
border-b-[2px]
border-sidebar-border
"
hx-trigger="{{printf "sse:accounts_%d_platforms" $acctID}}"
hx-get="{{printf "/ui/accounts/%d" $acctID}}"
hx-select="#shops-section"
hx-swap="morph"
>
<details class="flex flex-col items-stretch" open>
<summary class="list-none cursor-pointer self-center">
@@ -70,7 +65,7 @@
</h2>
</summary>
<ul class="list-none">
<ul class="list-none" hx-swap="morph">
{{- range $i, $platform := .Accounts.GetOrderOfPlatformsOnAccountPage $acctID }}
{{ component (printf "/accounts/%d/shops/list-items/%s" $acctID $platform) "OrderIndex" $i }}
{{- end }}