account page: can now reorder entries under shops sections by drag and drop
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"ruben/inventory2/internal/consts"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// TODO: simpify, if possible (single query ideal)
|
||||
func (db *Store) SetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int64, platform Platform, orderIndex int) error {
|
||||
tx, err := db.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// look up specified indexes
|
||||
|
||||
rows, err := tx.Query(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
platform,
|
||||
order_index
|
||||
FROM
|
||||
accounts_page_platform_order_indexes
|
||||
WHERE
|
||||
account_id = @account_id
|
||||
ORDER BY
|
||||
order_index ASC
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"account_id": acctID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to perform query to look up existing indexes: %w", err)
|
||||
}
|
||||
|
||||
indexes, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
|
||||
Platform Platform
|
||||
Order_index int
|
||||
}])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to scan rows for query to look up existing indexes: %w", err)
|
||||
}
|
||||
|
||||
// compute the implied indexes,
|
||||
// and construct the full sequence of platforms
|
||||
|
||||
indexPerPlatform := make(map[Platform]int, len(allPlatforms))
|
||||
platformPerIndex := make(map[int]Platform, len(allPlatforms))
|
||||
for _, v := range indexes {
|
||||
indexPerPlatform[v.Platform] = v.Order_index
|
||||
platformPerIndex[v.Order_index] = v.Platform
|
||||
}
|
||||
|
||||
prevIndex := -1
|
||||
for _, p := range allPlatforms {
|
||||
if _, indexSet := indexPerPlatform[p]; !indexSet {
|
||||
index := prevIndex
|
||||
for indexUsed := true; indexUsed; _, indexUsed = platformPerIndex[index] {
|
||||
index += 1
|
||||
}
|
||||
prevIndex = index
|
||||
|
||||
indexPerPlatform[p] = index
|
||||
platformPerIndex[index] = p
|
||||
}
|
||||
}
|
||||
|
||||
prevOrderIndex := indexPerPlatform[platform]
|
||||
if indexPerPlatform[platform] == orderIndex {
|
||||
return nil
|
||||
}
|
||||
|
||||
if increased := orderIndex > prevOrderIndex; increased {
|
||||
// decrement in indexes between the previous index and new index
|
||||
for index := prevOrderIndex + 1; index <= orderIndex; index += 1 {
|
||||
p := platformPerIndex[index]
|
||||
indexPerPlatform[p] = index - 1
|
||||
platformPerIndex[index-1] = p
|
||||
}
|
||||
indexPerPlatform[platform] = orderIndex
|
||||
platformPerIndex[orderIndex] = platform
|
||||
} else {
|
||||
// increment in indexes between the previous index and new index
|
||||
for index := prevOrderIndex - 1; index >= orderIndex; index -= 1 {
|
||||
p := platformPerIndex[index]
|
||||
indexPerPlatform[p] = index + 1
|
||||
platformPerIndex[index+1] = p
|
||||
}
|
||||
indexPerPlatform[platform] = orderIndex
|
||||
platformPerIndex[orderIndex] = platform
|
||||
}
|
||||
|
||||
// delete all indexes for the acct in the db,
|
||||
// then insert all updated indexes
|
||||
|
||||
valuesLines := make([]string, len(allPlatforms))
|
||||
args := pgx.NamedArgs{
|
||||
"account_id": acctID,
|
||||
}
|
||||
for i, p := range allPlatforms {
|
||||
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]
|
||||
}
|
||||
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
fmt.Sprintf(`
|
||||
WITH deleted_indexes AS (
|
||||
DELETE FROM
|
||||
accounts_page_platform_order_indexes
|
||||
WHERE
|
||||
account_id = @account_id
|
||||
RETURNING
|
||||
account_id
|
||||
), new_indexes(account_id, platform, order_index) AS (
|
||||
SELECT DISTINCT
|
||||
x.account_id, x.platform, x.order_index
|
||||
FROM
|
||||
(VALUES %s) AS x(account_id, platform, order_index)
|
||||
LEFT JOIN
|
||||
deleted_indexes
|
||||
ON
|
||||
x.account_id = deleted_indexes.account_id
|
||||
)
|
||||
INSERT INTO
|
||||
accounts_page_platform_order_indexes (
|
||||
account_id,
|
||||
platform,
|
||||
order_index
|
||||
)
|
||||
SELECT
|
||||
account_id,
|
||||
platform::Platform,
|
||||
order_index
|
||||
FROM
|
||||
new_indexes
|
||||
`, strings.Join(valuesLines, ", ")),
|
||||
args,
|
||||
)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
func (db *Store) GetOrderOfPlatformsOnAccountPage(ctx context.Context, acctID int64) ([]Platform, error) {
|
||||
// get specified order indexes per platform
|
||||
rows, err := db.db.Query(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
platform,
|
||||
order_index
|
||||
FROM
|
||||
accounts_page_platform_order_indexes
|
||||
WHERE
|
||||
account_id = @account_id
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"account_id": acctID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform query: %w", err)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
Platform Platform
|
||||
Order_index int
|
||||
}
|
||||
|
||||
platformsWithIndexes, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
|
||||
Platform Platform
|
||||
Order_index int
|
||||
}])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan rows: %w", err)
|
||||
}
|
||||
|
||||
// hash the specified platforms by index
|
||||
|
||||
platformsByIndex := make(map[int]Platform, len(platformsWithIndexes))
|
||||
for _, v := range platformsWithIndexes {
|
||||
platformsByIndex[v.Order_index] = v.Platform
|
||||
}
|
||||
|
||||
// form a sorted list of platforms without indexes specified, sorted alphabetically
|
||||
|
||||
allPlatformsByName := make(map[Platform]struct{}, len(allPlatforms))
|
||||
for _, p := range allPlatforms {
|
||||
allPlatformsByName[p] = struct{}{}
|
||||
}
|
||||
for _, v := range platformsWithIndexes {
|
||||
delete(allPlatformsByName, v.Platform)
|
||||
}
|
||||
platformsWithoutAnIndexSpecified := make([]Platform, 0, len(allPlatformsByName))
|
||||
for p := range allPlatformsByName {
|
||||
platformsWithoutAnIndexSpecified = append(platformsWithoutAnIndexSpecified, p)
|
||||
}
|
||||
slices.SortFunc(platformsWithoutAnIndexSpecified, func(a, b Platform) int {
|
||||
al := strings.ToLower(string(a))
|
||||
bl := strings.ToLower(string(b))
|
||||
if al < bl {
|
||||
return -1
|
||||
}
|
||||
if bl < al {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// construct the list of the ordered platforms
|
||||
|
||||
res := make([]Platform, len(allPlatforms))
|
||||
nextIndex := 0
|
||||
for i := range res {
|
||||
if p, ok := platformsByIndex[i]; ok {
|
||||
res[i] = p
|
||||
} else {
|
||||
res[i] = platformsWithoutAnIndexSpecified[nextIndex]
|
||||
nextIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (db *Store) GetOrderOfPlatformOnAccountPage(ctx context.Context, acctID int64, platform Platform) (int, error) {
|
||||
rows, err := db.db.Query(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
order_index
|
||||
FROM
|
||||
accounts_page_platform_order_indexes
|
||||
WHERE
|
||||
account_id = @account_id
|
||||
AND platform = @platform
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"account_id": acctID,
|
||||
"platform": platform,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to perform query: %w", err)
|
||||
}
|
||||
|
||||
i, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, consts.ErrNotFound
|
||||
}
|
||||
return 0, fmt.Errorf("failed to scan rows: %w", err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
@@ -10,41 +10,41 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
Etsy Platform = "Etsy"
|
||||
Tiktok Platform = "Tiktok"
|
||||
Wix Platform = "Wix"
|
||||
Ebay Platform = "ebay"
|
||||
WalmartMarketplace Platform = "walmart_marketplace"
|
||||
Amazon Platform = "amazon"
|
||||
BigCartel Platform = "big_cartel"
|
||||
Ebay Platform = "ebay"
|
||||
Ecwid Platform = "ecwid"
|
||||
Zoho Platform = "zoho"
|
||||
Etsy Platform = "Etsy"
|
||||
Shopify Platform = "shopify"
|
||||
SquareOnline Platform = "square_online"
|
||||
Squarespace Platform = "squarespace"
|
||||
Tiktok Platform = "Tiktok"
|
||||
WalmartMarketplace Platform = "walmart_marketplace"
|
||||
Wix Platform = "Wix"
|
||||
WooCommerce Platform = "woo_commerce"
|
||||
Shopify Platform = "shopify"
|
||||
Zoho Platform = "zoho"
|
||||
)
|
||||
|
||||
var (
|
||||
allValues = []Platform{
|
||||
Etsy,
|
||||
Tiktok,
|
||||
Wix,
|
||||
Ebay,
|
||||
WalmartMarketplace,
|
||||
allPlatforms = []Platform{
|
||||
Amazon,
|
||||
BigCartel,
|
||||
Ebay,
|
||||
Ecwid,
|
||||
Zoho,
|
||||
Etsy,
|
||||
Shopify,
|
||||
SquareOnline,
|
||||
Squarespace,
|
||||
Tiktok,
|
||||
WalmartMarketplace,
|
||||
Wix,
|
||||
WooCommerce,
|
||||
Shopify,
|
||||
Zoho,
|
||||
}
|
||||
)
|
||||
|
||||
func NewPlatform(s string) (Platform, error) {
|
||||
for _, v := range allValues {
|
||||
for _, v := range allPlatforms {
|
||||
if strings.ToLower(s) == strings.ToLower(string(v)) {
|
||||
return v, nil
|
||||
}
|
||||
@@ -52,6 +52,38 @@ func NewPlatform(s string) (Platform, error) {
|
||||
return "", fmt.Errorf("unrecognized constant: %q", s)
|
||||
}
|
||||
|
||||
func (p Platform) PrettyPrint() string {
|
||||
switch p {
|
||||
case Etsy:
|
||||
return "Etsy"
|
||||
case Tiktok:
|
||||
return "Tiktok"
|
||||
case Wix:
|
||||
return "Wix"
|
||||
case Ebay:
|
||||
return "Ebay"
|
||||
case WalmartMarketplace:
|
||||
return "Walmart Marketplace"
|
||||
case Amazon:
|
||||
return "Amazon"
|
||||
case BigCartel:
|
||||
return "Big Cartel"
|
||||
case Ecwid:
|
||||
return "Ecwid"
|
||||
case Zoho:
|
||||
return "Zoho"
|
||||
case SquareOnline:
|
||||
return "Square Online"
|
||||
case Squarespace:
|
||||
return "Squarespace"
|
||||
case WooCommerce:
|
||||
return "Woo Commerce"
|
||||
case Shopify:
|
||||
return "Shopify"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// sql.Scanner implementation
|
||||
func (p *Platform) Scan(src any) error {
|
||||
var s string
|
||||
|
||||
@@ -51,6 +51,18 @@ 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 {
|
||||
return v_ctx.Store.SetOrderOfPlatformOnAccountPage(v_ctx.ctx, acctID, platform, orderIndex)
|
||||
}
|
||||
|
||||
func (v_ctx *StoreWithContext) GetOrderOfPlatformsOnAccountPage(acctID int64) ([]Platform, error) {
|
||||
return v_ctx.Store.GetOrderOfPlatformsOnAccountPage(v_ctx.ctx, acctID)
|
||||
}
|
||||
|
||||
func (v_ctx *StoreWithContext) GetOrderOfPlatformOnAccountPage(acctID int64, platform Platform) (int, error) {
|
||||
return v_ctx.Store.GetOrderOfPlatformOnAccountPage(v_ctx.ctx, acctID, platform)
|
||||
}
|
||||
|
||||
func (v_ctx *StoreWithContext) CreateSyncGroupListingDraft(acctID int64) (int, error) {
|
||||
return v_ctx.Store.CreateSyncGroupListingDraft(v_ctx.ctx, acctID)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package accounts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -32,9 +33,10 @@ func Routes(
|
||||
}
|
||||
|
||||
r.POST("", response.Handler(as.createAccount))
|
||||
r.PUT("/:acctID/platforms/:platform/order-index", pub.Publish("/:acctID/platforms"), response.Handler(as.setOrderOfPlatformOnAccountPage))
|
||||
|
||||
syncGroups := r.Group("/:acctID/inventory/sync-groups")
|
||||
syncGroups.POST("", response.Handler(as.saveNewSyncGroup))
|
||||
syncGroups.POST("", pub.Publish("/:acctID/inventory/sync-groups"), response.Handler(as.saveNewSyncGroup))
|
||||
|
||||
draftListings := syncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/draft/listings"))
|
||||
draftListings.POST("", response.Handler(as.createSyncGroupListingDraft))
|
||||
@@ -184,3 +186,39 @@ func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error)
|
||||
|
||||
return orderIndex, nil
|
||||
}
|
||||
|
||||
func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (response.Response, error) {
|
||||
acctID := auth.GetIdentity(c).Account.AccountID
|
||||
|
||||
var orderIndex int
|
||||
if v, ok := c.GetPostForm("order-index"); !ok {
|
||||
return nil, response.BadRequest().
|
||||
Msg("no order-index provided")
|
||||
} else if i, err := strconv.Atoi(v); err != nil {
|
||||
return nil, response.BadRequest().
|
||||
Msgf("order-index must be a non-negative integer: %s", v)
|
||||
} else if i < 0 {
|
||||
return nil, response.BadRequest().
|
||||
Msgf("order-index must be a non-negative integer: %s", v)
|
||||
} else {
|
||||
orderIndex = i
|
||||
}
|
||||
|
||||
var platform accounts.Platform
|
||||
if v := c.Param("platform"); v == "" {
|
||||
return nil, response.BadRequest().
|
||||
Msg("no platform provided")
|
||||
} else if p, err := accounts.NewPlatform(v); err != nil {
|
||||
return nil, response.BadRequest().
|
||||
Wrap(err).
|
||||
Msgf("unrecognized platform: %v", v)
|
||||
} else {
|
||||
platform = p
|
||||
}
|
||||
|
||||
if err := s.accts.SetOrderOfPlatformOnAccountPage(c, acctID, platform, orderIndex); err != nil {
|
||||
return nil, fmt.Errorf("failed to save record: %w", err)
|
||||
}
|
||||
|
||||
return response.Status(200), nil
|
||||
}
|
||||
|
||||
@@ -150,7 +150,13 @@ func (e ErrorResponse) GetHTML() ([]byte, bool) {
|
||||
}
|
||||
|
||||
func GetError(err error) (e ErrorResponse, ok bool) {
|
||||
ok = errors.As(err, &e)
|
||||
if ok = errors.As(err, &e); ok {
|
||||
return e, true
|
||||
}
|
||||
var ptr *ErrorResponse
|
||||
if ok = errors.As(err, &ptr); ok {
|
||||
return *ptr, true
|
||||
}
|
||||
return e, ok
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -45,6 +46,11 @@ func HandleErrors(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
var err error
|
||||
for _, e := range c.Errors {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user