account page: can now reorder entries under shops sections by drag and drop
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE accounts_page_platform_order_indexes;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE accounts_page_platform_order_indexes (
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
platform platform NOT NULL,
|
||||||
|
order_index SMALLINT NOT NULL CHECK (order_index >= 0),
|
||||||
|
|
||||||
|
PRIMARY KEY (account_id, platform),
|
||||||
|
UNIQUE (account_id, order_index)
|
||||||
|
);
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 58 KiB |
@@ -11,6 +11,14 @@ entity "**accounts**" {
|
|||||||
*""user_id"": //text [FK]//
|
*""user_id"": //text [FK]//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**accounts_page_platform_order_indexes**" {
|
||||||
|
+ ""account_id"": //integer [PK]//
|
||||||
|
+ ""platform"": //platform [PK]//
|
||||||
|
--
|
||||||
|
*""account_id"": //integer //
|
||||||
|
*""order_index"": //smallint //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**etsy_access_tokens**" {
|
entity "**etsy_access_tokens**" {
|
||||||
+ ""user_id"": //integer [PK][FK]//
|
+ ""user_id"": //integer [PK][FK]//
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -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 (
|
const (
|
||||||
Etsy Platform = "Etsy"
|
|
||||||
Tiktok Platform = "Tiktok"
|
|
||||||
Wix Platform = "Wix"
|
|
||||||
Ebay Platform = "ebay"
|
|
||||||
WalmartMarketplace Platform = "walmart_marketplace"
|
|
||||||
Amazon Platform = "amazon"
|
Amazon Platform = "amazon"
|
||||||
BigCartel Platform = "big_cartel"
|
BigCartel Platform = "big_cartel"
|
||||||
|
Ebay Platform = "ebay"
|
||||||
Ecwid Platform = "ecwid"
|
Ecwid Platform = "ecwid"
|
||||||
Zoho Platform = "zoho"
|
Etsy Platform = "Etsy"
|
||||||
|
Shopify Platform = "shopify"
|
||||||
SquareOnline Platform = "square_online"
|
SquareOnline Platform = "square_online"
|
||||||
Squarespace Platform = "squarespace"
|
Squarespace Platform = "squarespace"
|
||||||
|
Tiktok Platform = "Tiktok"
|
||||||
|
WalmartMarketplace Platform = "walmart_marketplace"
|
||||||
|
Wix Platform = "Wix"
|
||||||
WooCommerce Platform = "woo_commerce"
|
WooCommerce Platform = "woo_commerce"
|
||||||
Shopify Platform = "shopify"
|
Zoho Platform = "zoho"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
allValues = []Platform{
|
allPlatforms = []Platform{
|
||||||
Etsy,
|
|
||||||
Tiktok,
|
|
||||||
Wix,
|
|
||||||
Ebay,
|
|
||||||
WalmartMarketplace,
|
|
||||||
Amazon,
|
Amazon,
|
||||||
BigCartel,
|
BigCartel,
|
||||||
|
Ebay,
|
||||||
Ecwid,
|
Ecwid,
|
||||||
Zoho,
|
Etsy,
|
||||||
|
Shopify,
|
||||||
SquareOnline,
|
SquareOnline,
|
||||||
Squarespace,
|
Squarespace,
|
||||||
|
Tiktok,
|
||||||
|
WalmartMarketplace,
|
||||||
|
Wix,
|
||||||
WooCommerce,
|
WooCommerce,
|
||||||
Shopify,
|
Zoho,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewPlatform(s string) (Platform, error) {
|
func NewPlatform(s string) (Platform, error) {
|
||||||
for _, v := range allValues {
|
for _, v := range allPlatforms {
|
||||||
if strings.ToLower(s) == strings.ToLower(string(v)) {
|
if strings.ToLower(s) == strings.ToLower(string(v)) {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,38 @@ func NewPlatform(s string) (Platform, error) {
|
|||||||
return "", fmt.Errorf("unrecognized constant: %q", s)
|
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
|
// sql.Scanner implementation
|
||||||
func (p *Platform) Scan(src any) error {
|
func (p *Platform) Scan(src any) error {
|
||||||
var s string
|
var s string
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ 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 {
|
||||||
|
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) {
|
func (v_ctx *StoreWithContext) CreateSyncGroupListingDraft(acctID int64) (int, error) {
|
||||||
return v_ctx.Store.CreateSyncGroupListingDraft(v_ctx.ctx, acctID)
|
return v_ctx.Store.CreateSyncGroupListingDraft(v_ctx.ctx, acctID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package accounts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -32,9 +33,10 @@ func Routes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.POST("", response.Handler(as.createAccount))
|
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 := 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 := syncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/draft/listings"))
|
||||||
draftListings.POST("", response.Handler(as.createSyncGroupListingDraft))
|
draftListings.POST("", response.Handler(as.createSyncGroupListingDraft))
|
||||||
@@ -184,3 +186,39 @@ func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error)
|
|||||||
|
|
||||||
return orderIndex, nil
|
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) {
|
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
|
return e, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -45,6 +46,11 @@ func HandleErrors(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
|
var err error
|
||||||
|
for _, e := range c.Errors {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
(function(){
|
||||||
|
/**
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
const htmx = window.htmx;
|
||||||
|
|
||||||
|
htmx.defineExtension("drag", {
|
||||||
|
init: (node) => {
|
||||||
|
// ideally it should only start listening to elements with hx-ext="drag"
|
||||||
|
// I assume there is a better way I can bind than this?
|
||||||
|
document.addEventListener("dragstart", DragStart);
|
||||||
|
document.addEventListener("dragover", DragOver);
|
||||||
|
document.addEventListener("dragleave", DragLeave);
|
||||||
|
document.addEventListener("dragend", DragEnd);
|
||||||
|
document.addEventListener("drop", Drop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the most recent element a drag event started on
|
||||||
|
* @type {Element|null}
|
||||||
|
*/
|
||||||
|
let dragging = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent element which matches the selector
|
||||||
|
* @param {EventTarget|null} target
|
||||||
|
* @param {string} selector
|
||||||
|
* @returns {Element | null}
|
||||||
|
*/
|
||||||
|
function GetTarget(target, selector) {
|
||||||
|
if (target == null) return null;
|
||||||
|
if (!(target instanceof Element)) return null;
|
||||||
|
|
||||||
|
target = target.closest(`[${selector}],[data-${selector}]`);
|
||||||
|
if (target == null) return null;
|
||||||
|
if (!(target instanceof Element)) return null;
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attribute allowing `data-` fallback
|
||||||
|
* @param {Element | null} target
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
function GetAttribute(target, name) {
|
||||||
|
if (target === null) return null;
|
||||||
|
return target.getAttribute(name) || target.getAttribute("data-"+name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} ev
|
||||||
|
*/
|
||||||
|
function DragStart(ev) {
|
||||||
|
if (!ev.dataTransfer) return;
|
||||||
|
|
||||||
|
const target = GetTarget(ev.target, "hx-drag");
|
||||||
|
if (!target) return null;
|
||||||
|
|
||||||
|
const data = GetAttribute(target, "hx-drag");
|
||||||
|
if (data === null) return;
|
||||||
|
|
||||||
|
target.classList.add("hx-drag");
|
||||||
|
dragging = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} ev
|
||||||
|
*/
|
||||||
|
function DragOver(ev) {
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
const target = GetTarget(ev.target, "hx-drop");
|
||||||
|
if (!target) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
target.classList.add("hx-drag-over");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} ev
|
||||||
|
*/
|
||||||
|
function DragLeave(ev) {
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
const target = GetTarget(ev.target, "hx-drop");
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
target.classList.remove("hx-drag-over");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} ev
|
||||||
|
*/
|
||||||
|
function DragEnd() {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging.classList.remove("hx-drag");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} ev
|
||||||
|
*/
|
||||||
|
async function Drop(ev) {
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
const drag = dragging;
|
||||||
|
const drop = GetTarget(ev.target, "hx-drop");
|
||||||
|
if (!drop) return;
|
||||||
|
drop.classList.add("hx-drop");
|
||||||
|
drop.classList.remove("hx-drag-over");
|
||||||
|
|
||||||
|
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") || "{}");
|
||||||
|
|
||||||
|
const precedence = GetAttribute(drag, "hx-drag-sync") || GetAttribute(drag, "hx-drop-sync");
|
||||||
|
const dragFirst = precedence === "drag";
|
||||||
|
const sync = !!precedence;
|
||||||
|
|
||||||
|
drop.classList.add("htmx-request");
|
||||||
|
drag.classList.add("htmx-request");
|
||||||
|
const queue = dragFirst ? [drag, drop] : [drop, drag];
|
||||||
|
for (const elm of queue) {
|
||||||
|
const promise = RunDragDrop(elm, elm === drag, dragVals, dropVals);
|
||||||
|
if (sync) await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
drag.classList.remove("hx-drag");
|
||||||
|
drop.classList.remove("hx-drop");
|
||||||
|
dragging = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Element} source
|
||||||
|
* @param {boolean} isDrag
|
||||||
|
* @param {object} dragVals
|
||||||
|
* @param {object} dropVals
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function RunDragDrop(source, isDrag, dragVals, dropVals) {
|
||||||
|
if (!document.body.contains(source)) return;
|
||||||
|
|
||||||
|
let method, action, values;
|
||||||
|
if (isDrag) {
|
||||||
|
action = GetAttribute(source, "hx-drag-action");
|
||||||
|
method = GetAttribute(source, "hx-drag-method") || "PUT";
|
||||||
|
values = Object.assign({}, dropVals, dragVals);
|
||||||
|
} else {
|
||||||
|
action = GetAttribute(source, "hx-drop-action");
|
||||||
|
method = GetAttribute(source, "hx-drop-method") || "PUT";
|
||||||
|
values = Object.assign({}, dragVals, dropVals);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('action:', action)
|
||||||
|
|
||||||
|
if (action === null) return;
|
||||||
|
|
||||||
|
try { // don't let one failure cascade to the other ajax
|
||||||
|
await htmx.ajax(method, action, { source, values });
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{{- $acctID := .Identity.Account.AccountID }}
|
||||||
|
{{- $platform := parsePlatform .PathParams.platform -}}
|
||||||
|
{{- $orderIndex := .OrderIndex }}
|
||||||
|
{{- if and (not $orderIndex) (ne .OrderIndex 0) }}
|
||||||
|
{{- $orderIndex = .Accounts.GetOrderOfPlatformOnAccountPage $acctID $platform -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{- $platformSegment := lowerCamelCase (printf "%s" $platform) }}
|
||||||
|
{{- $path := 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}}"}'
|
||||||
|
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')"
|
||||||
|
draggable="true"
|
||||||
|
>
|
||||||
|
<details
|
||||||
|
open
|
||||||
|
class="
|
||||||
|
group
|
||||||
|
|
||||||
|
details-content:opacity-[0]
|
||||||
|
open:details-content:opacity-[1]
|
||||||
|
|
||||||
|
details-content:transform-[translate(0px,-2em)]
|
||||||
|
open:details-content:transform-[translate(0px,0px)]
|
||||||
|
|
||||||
|
details-content:border-transparent
|
||||||
|
open:details-content:border-dotted
|
||||||
|
open:details-content:border-x-[0px]
|
||||||
|
open:details-content:border-x-transparent
|
||||||
|
open:details-content:border-b-[0px]
|
||||||
|
open:details-content:border-b-transparent
|
||||||
|
open:details-content:border-t-background
|
||||||
|
open:details-content:border-t-[0.25rem]
|
||||||
|
details-content:outline-transparent
|
||||||
|
|
||||||
|
details-content:transition-[opacity_transform]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<summary class="
|
||||||
|
list-none
|
||||||
|
p-[1em]
|
||||||
|
after:content-['+']
|
||||||
|
group-open:after:content-['-']
|
||||||
|
after:float-right
|
||||||
|
after:font-bold
|
||||||
|
after:text-[2em]
|
||||||
|
after:leading-[1em]
|
||||||
|
cursor-pointer
|
||||||
|
">
|
||||||
|
{{/*
|
||||||
|
group-open:border-b-background
|
||||||
|
group-open:border-b-[0.25rem]
|
||||||
|
border-dotted
|
||||||
|
*/}}
|
||||||
|
<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>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<script src="/scripts/htmx-ext-sse.js"></script>
|
<script src="/scripts/htmx-ext-sse.js"></script>
|
||||||
<script src="/scripts/htmx-ext-path-params.js"></script>
|
<script src="/scripts/htmx-ext-path-params.js"></script>
|
||||||
<script src="/scripts/idiomorph-ext.js"></script>
|
<script src="/scripts/idiomorph-ext.js"></script>
|
||||||
|
<script src="/scripts/hx-drag.js"></script>
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
|
|
||||||
<body
|
<body
|
||||||
class="basis-full grow bg-background flex flex-col items-stretch"
|
class="basis-full grow bg-background flex flex-col items-stretch"
|
||||||
hx-ext="morph,path-params,sse"
|
hx-ext="morph,path-params,sse,drag"
|
||||||
{{/* get requests to just replace the body - until we start replacing the head, this will be good */}}
|
{{/* get requests to just replace the body - until we start replacing the head, this will be good */}}
|
||||||
hx-boost="true"
|
hx-boost="true"
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,20 @@
|
|||||||
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
|
id="shops-section"
|
||||||
class="
|
class="
|
||||||
m-[1rem]
|
m-[1rem]
|
||||||
pb-[1rem]
|
pb-[1rem]
|
||||||
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">
|
<details class="flex flex-col items-stretch" open>
|
||||||
<summary class="list-none cursor-pointer self-center">
|
<summary class="list-none cursor-pointer self-center">
|
||||||
<h2 class="bg-card my-[0.5em] px-[2em] py-[0.5em] rounded-lg">
|
<h2 class="bg-card my-[0.5em] px-[2em] py-[0.5em] rounded-lg">
|
||||||
Shops
|
Shops
|
||||||
@@ -65,74 +71,9 @@
|
|||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<ul class="list-none">
|
<ul class="list-none">
|
||||||
{{- define "shop-list-entry" }}
|
{{- range $i, $platform := .Accounts.GetOrderOfPlatformsOnAccountPage $acctID }}
|
||||||
{{/* .Platform, .AccountID */}}
|
{{ component (printf "/accounts/%d/shops/list-items/%s" $acctID $platform) "OrderIndex" $i }}
|
||||||
<li class="my-[1rem] rounded-lg bg-accent">
|
|
||||||
<details
|
|
||||||
class="
|
|
||||||
group
|
|
||||||
|
|
||||||
details-content:opacity-[0]
|
|
||||||
open:details-content:opacity-[1]
|
|
||||||
|
|
||||||
details-content:transform-[translate(0px,-2em)]
|
|
||||||
open:details-content:transform-[translate(0px,0px)]
|
|
||||||
|
|
||||||
details-content:border-transparent
|
|
||||||
open:details-content:border-dotted
|
|
||||||
open:details-content:border-x-[0px]
|
|
||||||
open:details-content:border-x-transparent
|
|
||||||
open:details-content:border-b-[0px]
|
|
||||||
open:details-content:border-b-transparent
|
|
||||||
open:details-content:border-t-background
|
|
||||||
open:details-content:border-t-[0.25rem]
|
|
||||||
details-content:outline-transparent
|
|
||||||
|
|
||||||
details-content:transition-[opacity_transform]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<summary class="
|
|
||||||
list-none
|
|
||||||
p-[1em]
|
|
||||||
after:content-['+']
|
|
||||||
group-open:after:content-['-']
|
|
||||||
after:float-right
|
|
||||||
after:font-bold
|
|
||||||
after:text-[2em]
|
|
||||||
after:leading-[1em]
|
|
||||||
cursor-pointer
|
|
||||||
">
|
|
||||||
{{/*
|
|
||||||
group-open:border-b-background
|
|
||||||
group-open:border-b-[0.25rem]
|
|
||||||
border-dotted
|
|
||||||
*/}}
|
|
||||||
<h3 class="inline-block">{{.Platform}}</h3>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<div class="p-[1em] text-center">
|
|
||||||
{{- $pathSegment := lowerCamelCase .Platform }}
|
|
||||||
{{-
|
|
||||||
component
|
|
||||||
(printf "/accounts/%d/shops/link-sections/%s" .AccountID $pathSegment)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</li>
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- template "shop-list-entry" (props "Platform" "Amazon" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Big Cartel" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "eBay" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Etsy" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Ecwid" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Shopify" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Square Online" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Squarespace" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Tiktok" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Walmart Marketplace" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Wix" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Woo Commerce" "AccountID" $acctID) }}
|
|
||||||
{{- template "shop-list-entry" (props "Platform" "Zoho" "AccountID" $acctID) }}
|
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user