account page: fixed platform reordering bugs
This commit is contained in:
@@ -12,10 +12,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TODO: simpify, if possible (single query ideal)
|
// 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)
|
tx, err := db.db.Begin(ctx)
|
||||||
if err != nil {
|
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)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
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 {
|
indexes, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
|
||||||
@@ -47,7 +52,7 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
|
|||||||
Order_index int
|
Order_index int
|
||||||
}])
|
}])
|
||||||
if err != nil {
|
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,
|
// 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 {
|
if indexPerPlatform[platform] == orderIndex {
|
||||||
return nil
|
for i := range allPlatforms {
|
||||||
|
orderedPlatforms[i] = platformPerIndex[i]
|
||||||
|
}
|
||||||
|
return orderedPlatforms, prevOrderIndex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if increased := orderIndex > prevOrderIndex; increased {
|
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)
|
valuesLines[i] = fmt.Sprintf("(@account_id, @platform_%d, @order_index_%d::smallint)", i, i)
|
||||||
args[fmt.Sprintf("platform_%d", i)] = p
|
args[fmt.Sprintf("platform_%d", i)] = p
|
||||||
args[fmt.Sprintf("order_index_%d", i)] = indexPerPlatform[p]
|
args[fmt.Sprintf("order_index_%d", i)] = indexPerPlatform[p]
|
||||||
|
orderedPlatforms[i] = platformPerIndex[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(
|
_, err = tx.Exec(
|
||||||
@@ -148,14 +158,14 @@ func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int
|
|||||||
args,
|
args,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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 {
|
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) {
|
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)
|
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)
|
return v_ctx.Store.SetOrderOfPlatformOnAccountPage(v_ctx.ctx, acctID, platform, orderIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
type accountSubrouter struct {
|
type accountSubrouter struct {
|
||||||
log *logging.Logger
|
log *logging.Logger
|
||||||
accts *accounts.Store
|
accts *accounts.Store
|
||||||
|
pub *sse.UpdateNotificationPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
func Routes(
|
func Routes(
|
||||||
@@ -30,6 +32,7 @@ func Routes(
|
|||||||
as := &accountSubrouter{
|
as := &accountSubrouter{
|
||||||
log: logger,
|
log: logger,
|
||||||
accts: accts,
|
accts: accts,
|
||||||
|
pub: pub,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.POST("", response.Handler(as.createAccount))
|
r.POST("", response.Handler(as.createAccount))
|
||||||
@@ -187,9 +190,13 @@ func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error)
|
|||||||
return orderIndex, nil
|
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) {
|
func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (response.Response, error) {
|
||||||
acctID := auth.GetIdentity(c).Account.AccountID
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
// validate parameters
|
||||||
|
|
||||||
var orderIndex int
|
var orderIndex int
|
||||||
if v, ok := c.GetPostForm("order-index"); !ok {
|
if v, ok := c.GetPostForm("order-index"); !ok {
|
||||||
return nil, response.BadRequest().
|
return nil, response.BadRequest().
|
||||||
@@ -216,9 +223,34 @@ func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (resp
|
|||||||
platform = p
|
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 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), " "), "_"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package sse
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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 {
|
func getPathSegments(p string) []string {
|
||||||
p = path.Clean(p)
|
p = path.Clean(p)
|
||||||
if p == "" || p == "." || p == "/" {
|
if p == "" || p == "." || p == "/" {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func Routes(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// strings
|
// strings
|
||||||
"lowerCamelCase": func(s string) string {
|
"lowerSnakeCase": func(s string) string {
|
||||||
return strings.ToLower(strings.Join(strings.Split(s, " "), "_"))
|
return strings.ToLower(strings.Join(strings.Split(s, " "), "_"))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -114,9 +114,6 @@
|
|||||||
const dragAttr = GetAttribute(drag, "hx-drag");
|
const dragAttr = GetAttribute(drag, "hx-drag");
|
||||||
const dropAttr = GetAttribute(drop, "hx-drop");
|
const dropAttr = GetAttribute(drop, "hx-drop");
|
||||||
|
|
||||||
console.log('dragAttr:', dragAttr);
|
|
||||||
console.log('dropAttr:', dropAttr);
|
|
||||||
|
|
||||||
const dragVals = JSON.parse(GetAttribute(drag, "hx-drag") || "{}");
|
const dragVals = JSON.parse(GetAttribute(drag, "hx-drag") || "{}");
|
||||||
const dropVals = JSON.parse(GetAttribute(drop, "hx-drop") || "{}");
|
const dropVals = JSON.parse(GetAttribute(drop, "hx-drop") || "{}");
|
||||||
|
|
||||||
@@ -159,8 +156,6 @@
|
|||||||
values = Object.assign({}, dragVals, dropVals);
|
values = Object.assign({}, dragVals, dropVals);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('action:', action)
|
|
||||||
|
|
||||||
if (action === null) return;
|
if (action === null) return;
|
||||||
|
|
||||||
try { // don't let one failure cascade to the other ajax
|
try { // don't let one failure cascade to the other ajax
|
||||||
|
|||||||
+46
-39
@@ -6,44 +6,48 @@
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
|
||||||
{{- $platformSegment := lowerCamelCase (printf "%s" $platform) }}
|
{{- $platformSegment := lowerSnakeCase (printf "%s" $platform) }}
|
||||||
{{- $path := printf "/accounts/%d/platforms/%s/order-index" $acctID $platformSegment }}
|
{{- $updatePath := printf "/accounts/%d/platforms/%s/order-index" $acctID $platformSegment }}
|
||||||
<li
|
<li
|
||||||
id="{{$platformSegment}}"
|
id="{{$platformSegment}}"
|
||||||
class="my-[1rem] rounded-lg bg-accent"
|
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-drag="{}"
|
||||||
hx-trigger="sse:{{$path}}"
|
hx-drop='{"order-index": "{{$orderIndex}}"}'
|
||||||
hx-get="/ui{{$path}}"
|
hx-drag-action={{printf "/api/accounts/%d/platforms/%s/order-index" $acctID $platformSegment}}
|
||||||
hx-on::sse-message="console.log('test')"
|
|
||||||
hx-on::sseMessage="console.log('test')"
|
hx-trigger={{printf "sse:accounts_%d_platforms_%s_order-index" $acctID $platformSegment }}
|
||||||
hx-on::sseOpen="console.log('test')"
|
hx-get={{printf "/ui/accounts/%d/shops/list-items/%s" $acctID $platformSegment}}
|
||||||
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')"
|
|
||||||
draggable="true"
|
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
|
<details
|
||||||
open
|
id="{{$platformSegment}}-details"
|
||||||
class="
|
class="
|
||||||
group
|
group
|
||||||
|
|
||||||
@@ -65,8 +69,10 @@
|
|||||||
|
|
||||||
details-content:transition-[opacity_transform]
|
details-content:transition-[opacity_transform]
|
||||||
"
|
"
|
||||||
|
hx-preserve
|
||||||
>
|
>
|
||||||
<summary class="
|
<summary
|
||||||
|
class="
|
||||||
list-none
|
list-none
|
||||||
p-[1em]
|
p-[1em]
|
||||||
after:content-['+']
|
after:content-['+']
|
||||||
@@ -76,7 +82,8 @@
|
|||||||
after:text-[2em]
|
after:text-[2em]
|
||||||
after:leading-[1em]
|
after:leading-[1em]
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
">
|
"
|
||||||
|
>
|
||||||
{{/*
|
{{/*
|
||||||
group-open:border-b-background
|
group-open:border-b-background
|
||||||
group-open:border-b-[0.25rem]
|
group-open:border-b-[0.25rem]
|
||||||
@@ -85,12 +92,12 @@
|
|||||||
<h3 class="inline-block">{{$platform.PrettyPrint}}</h3>
|
<h3 class="inline-block">{{$platform.PrettyPrint}}</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="p-[1em] text-center">
|
<div
|
||||||
{{- $pathSegment := lowerCamelCase (printf "%s" $platform) }}
|
id="{{$platform}}-tab"
|
||||||
{{-
|
class="p-[1em] text-center"
|
||||||
component
|
>
|
||||||
(printf "/accounts/%d/shops/link-sections/%s" $acctID $pathSegment)
|
{{- $pathSegment := lowerSnakeCase (printf "%s" $platform) }}
|
||||||
}}
|
{{- component (printf "/accounts/%d/shops/link-sections/%s" $acctID $pathSegment) }}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -57,11 +57,6 @@
|
|||||||
border-b-[2px]
|
border-b-[2px]
|
||||||
border-sidebar-border
|
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>
|
<details class="flex flex-col items-stretch" open>
|
||||||
<summary class="list-none cursor-pointer self-center">
|
<summary class="list-none cursor-pointer self-center">
|
||||||
@@ -70,7 +65,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<ul class="list-none">
|
<ul class="list-none" hx-swap="morph">
|
||||||
{{- range $i, $platform := .Accounts.GetOrderOfPlatformsOnAccountPage $acctID }}
|
{{- range $i, $platform := .Accounts.GetOrderOfPlatformsOnAccountPage $acctID }}
|
||||||
{{ component (printf "/accounts/%d/shops/list-items/%s" $acctID $platform) "OrderIndex" $i }}
|
{{ component (printf "/accounts/%d/shops/list-items/%s" $acctID $platform) "OrderIndex" $i }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user