mock sync group editing: edit, save, and delete listings

This commit is contained in:
2026-02-25 22:41:36 -07:00
parent ae031411b8
commit 85817a4adb
12 changed files with 1533 additions and 239 deletions
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 414 KiB

+225
View File
@@ -28,6 +28,15 @@ entity "**shop_amazon_listings**" {
*""description"": //text //
}
entity "**shop_amazon_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_amazon_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -65,6 +74,15 @@ entity "**shop_big_cartel_listings**" {
*""description"": //text //
}
entity "**shop_big_cartel_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_big_cartel_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -102,6 +120,15 @@ entity "**shop_ebay_listings**" {
*""description"": //text //
}
entity "**shop_ebay_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_ebay_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
@@ -139,6 +166,15 @@ entity "**shop_ecwid_listings**" {
*""description"": //text //
}
entity "**shop_ecwid_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_ecwid_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -176,6 +212,15 @@ entity "**shop_etsy_listings**" {
*""description"": //text //
}
entity "**shop_etsy_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_etsy_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
@@ -213,6 +258,15 @@ entity "**shop_shopify_listings**" {
*""description"": //text //
}
entity "**shop_shopify_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_shopify_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -250,6 +304,15 @@ entity "**shop_square_online_listings**" {
*""description"": //text //
}
entity "**shop_square_online_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_square_online_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -287,6 +350,15 @@ entity "**shop_squarespace_listings**" {
*""description"": //text //
}
entity "**shop_squarespace_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_squarespace_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -324,6 +396,15 @@ entity "**shop_tiktok_listings**" {
*""description"": //text //
}
entity "**shop_tiktok_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_tiktok_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -361,6 +442,15 @@ entity "**shop_walmart_marketplace_listings**" {
*""description"": //text //
}
entity "**shop_walmart_marketplace_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_walmart_marketplace_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -398,6 +488,15 @@ entity "**shop_wix_listings**" {
*""description"": //text //
}
entity "**shop_wix_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_wix_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
@@ -435,6 +534,15 @@ entity "**shop_woo_commerce_listings**" {
*""description"": //text //
}
entity "**shop_woo_commerce_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text //
""listing_id"": //text //
}
entity "**shop_woo_commerce_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
@@ -472,6 +580,15 @@ entity "**shop_zoho_listings**" {
*""description"": //text //
}
entity "**shop_zoho_sync_group_editing_listings**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""shop_id"": //text [FK]//
""listing_id"": //text //
}
entity "**shop_zoho_sync_group_listing_drafts**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
@@ -490,6 +607,26 @@ entity "**shop_zoho_sync_group_listings**" {
*""order_index"": //integer //
}
entity "**sync_group_drafts**" {
+ ""account_id"": //integer [PK][FK]//
--
""sync_group_id"": //integer [FK]//
}
entity "**sync_group_editing**" {
+ ""account_id"": //integer [PK][FK]//
--
*""account_id"": //integer //
*""sync_group_id"": //integer [FK]//
*""sync_group_id"": //integer //
}
entity "**sync_group_editing_listing_order_indexes**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
--
}
entity "**sync_group_listing_draft_order_indexes**" {
+ ""account_id"": //integer [PK][FK]//
+ ""order_index"": //integer [PK]//
@@ -521,6 +658,12 @@ entity "**sync_groups**" {
"**shop_amazon_listings**" ||-|| "**shop_amazon**"
"**shop_amazon_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_amazon_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_amazon_sync_group_editing_listings**" }-- "**shop_amazon**"
"**shop_amazon_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_amazon_sync_group_listing_drafts**" }-- "**accounts**"
@@ -545,6 +688,12 @@ entity "**sync_groups**" {
"**shop_big_cartel_listings**" ||-|| "**shop_big_cartel**"
"**shop_big_cartel_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_big_cartel_sync_group_editing_listings**" }-- "**shop_big_cartel**"
"**shop_big_cartel_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_big_cartel_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_big_cartel_sync_group_listing_drafts**" }-- "**accounts**"
@@ -569,6 +718,12 @@ entity "**sync_groups**" {
"**shop_ebay_listings**" ||-|| "**shop_ebay**"
"**shop_ebay_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_ebay_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_ebay_sync_group_editing_listings**" }-- "**shop_ebay**"
"**shop_ebay_sync_group_listing_drafts**" }-- "**accounts**"
"**shop_ebay_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
@@ -593,6 +748,12 @@ entity "**sync_groups**" {
"**shop_ecwid_listings**" ||-|| "**shop_ecwid**"
"**shop_ecwid_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_ecwid_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_ecwid_sync_group_editing_listings**" }-- "**shop_ecwid**"
"**shop_ecwid_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_ecwid_sync_group_listing_drafts**" }-- "**accounts**"
@@ -617,6 +778,12 @@ entity "**sync_groups**" {
"**shop_etsy_listings**" ||-|| "**shop_etsy**"
"**shop_etsy_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_etsy_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_etsy_sync_group_editing_listings**" }-- "**shop_etsy**"
"**shop_etsy_sync_group_listing_drafts**" }-- "**accounts**"
"**shop_etsy_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
@@ -641,6 +808,12 @@ entity "**sync_groups**" {
"**shop_shopify_listings**" ||-|| "**shop_shopify**"
"**shop_shopify_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_shopify_sync_group_editing_listings**" }-- "**shop_shopify**"
"**shop_shopify_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_shopify_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_shopify_sync_group_listing_drafts**" }-- "**accounts**"
@@ -665,6 +838,12 @@ entity "**sync_groups**" {
"**shop_square_online_listings**" ||-|| "**shop_square_online**"
"**shop_square_online_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_square_online_sync_group_editing_listings**" }-- "**shop_square_online**"
"**shop_square_online_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_square_online_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_square_online_sync_group_listing_drafts**" }-- "**accounts**"
@@ -689,6 +868,12 @@ entity "**sync_groups**" {
"**shop_squarespace_listings**" ||-|| "**shop_squarespace**"
"**shop_squarespace_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_squarespace_sync_group_editing_listings**" }-- "**shop_squarespace**"
"**shop_squarespace_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_squarespace_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_squarespace_sync_group_listing_drafts**" }-- "**accounts**"
@@ -713,6 +898,12 @@ entity "**sync_groups**" {
"**shop_tiktok_listings**" ||-|| "**shop_tiktok**"
"**shop_tiktok_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_tiktok_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_tiktok_sync_group_editing_listings**" }-- "**shop_tiktok**"
"**shop_tiktok_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_tiktok_sync_group_listing_drafts**" }-- "**accounts**"
@@ -737,6 +928,12 @@ entity "**sync_groups**" {
"**shop_walmart_marketplace_listings**" ||-|| "**shop_walmart_marketplace**"
"**shop_walmart_marketplace_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_walmart_marketplace_sync_group_editing_listings**" }-- "**shop_walmart_marketplace**"
"**shop_walmart_marketplace_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_walmart_marketplace_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_walmart_marketplace_sync_group_listing_drafts**" }-- "**accounts**"
@@ -761,6 +958,12 @@ entity "**sync_groups**" {
"**shop_wix_listings**" ||-|| "**shop_wix**"
"**shop_wix_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_wix_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_wix_sync_group_editing_listings**" }-- "**shop_wix**"
"**shop_wix_sync_group_listing_drafts**" }-- "**accounts**"
"**shop_wix_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
@@ -785,6 +988,12 @@ entity "**sync_groups**" {
"**shop_woo_commerce_listings**" ||-|| "**shop_woo_commerce**"
"**shop_woo_commerce_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_woo_commerce_sync_group_editing_listings**" }-- "**shop_woo_commerce**"
"**shop_woo_commerce_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_woo_commerce_sync_group_listing_drafts**" ||-|| "**sync_group_listing_draft_order_indexes**"
"**shop_woo_commerce_sync_group_listing_drafts**" }-- "**accounts**"
@@ -809,6 +1018,12 @@ entity "**sync_groups**" {
"**shop_zoho_listings**" ||-|| "**shop_zoho**"
"**shop_zoho_sync_group_editing_listings**" ||-|| "**sync_group_editing_listing_order_indexes**"
"**shop_zoho_sync_group_editing_listings**" }-- "**sync_group_editing**"
"**shop_zoho_sync_group_editing_listings**" }-- "**shop_zoho**"
"**shop_zoho_sync_group_listing_drafts**" }-- "**accounts**"
"**shop_zoho_sync_group_listing_drafts**" }-- "**sync_group_listing_draft_order_indexes**"
@@ -823,6 +1038,16 @@ entity "**sync_groups**" {
"**shop_zoho_sync_group_listings**" }-- "**sync_group_listing_order_indexes**"
"**sync_group_drafts**" ||-|| "**accounts**"
"**sync_group_drafts**" }-- "**sync_groups**"
"**sync_group_editing**" ||-|| "**accounts**"
"**sync_group_editing**" }-- "**sync_groups**"
"**sync_group_editing_listing_order_indexes**" }-- "**sync_group_editing**"
"**sync_group_listing_draft_order_indexes**" }-- "**accounts**"
"**sync_group_listing_order_indexes**" }-- "**sync_groups**"
+760
View File
@@ -7,6 +7,8 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
@@ -45,6 +47,11 @@ type (
Description string
Count int64
}
ListingWithOrderIndex struct {
Listing
OrderIndex int
}
)
func NewStore(logger *logging.Logger, db *pgxpool.Pool) *Store {
@@ -648,6 +655,212 @@ func (db *Store) StartEditingMockSyncGroup(ctx context.Context, acctID, syncGrou
return v.Int64, v.Valid, nil
}
func (db *Store) SaveMockSyncGroupBeingEdited(ctx context.Context, acctID int64) (syncGroupID int64, err error) {
err = db.beginTxn(ctx, func(tx pgx.Tx) error {
// delete old sync group listings
rows, err := tx.Query(
ctx,
`
WITH the_sync_group AS (
SELECT
sync_group_id
FROM
mock.sync_group_editing
WHERE
account_id = @account_id
), deleted_indexes AS (
DELETE FROM
mock.sync_group_listing_order_indexes ind
USING
mock.sync_group_editing AS ed
WHERE
ed.account_id = @account_id
AND ind.sync_group_id = ed.sync_group_id
RETURNING
sync_group_id
)
SELECT
sync_group_id
FROM
deleted_indexes
LIMIT
1
`,
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to query editing sync group: %w", err)
}
if syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan row: %w", err)
}
var listingCount int
for _, in := range getAllMockShopSchemaInfos() {
// look up edited sync group listings
rows, err := tx.Query(
ctx,
fmt.Sprintf(
`
SELECT
order_index,
shop_id,
listing_id
FROM
%s el
WHERE
account_id = @account_id
`,
in.syncGroupEditingListingTable,
),
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
Order_index int
Shop_id pgtype.Text
Listing_id pgtype.Text
}])
if err != nil {
return fmt.Errorf("failed to scan rows: %w", err)
}
if len(vs) == 0 {
continue
}
// fail if any shop_id's or listing_id's are NULL
for _, v := range vs {
if !v.Shop_id.Valid {
return fmt.Errorf("%w: shop id missing on listing", consts.ErrConflict)
}
if !v.Listing_id.Valid {
return fmt.Errorf("%w: listing id missing on listing", consts.ErrConflict)
}
}
// create new sync group listing order indexes
values := make([]string, len(vs))
args := make(pgx.NamedArgs, len(vs))
for i, v := range vs {
values[i] = fmt.Sprintf("(@sync_group_id_%[1]d, @order_index_%[1]d)", i)
args[fmt.Sprintf("sync_group_id_%d", i)] = syncGroupID
args[fmt.Sprintf("order_index_%d", i)] = v.Order_index
}
listingCount += len(vs)
_, err = tx.Exec(
ctx,
fmt.Sprintf(
`
INSERT INTO
mock.sync_group_listing_order_indexes (
sync_group_id,
order_index
)
VALUES
%s
`,
strings.Join(values, ", "),
),
args,
)
if err != nil {
return fmt.Errorf("failed to delete rows for sync group listing order indexes: %w", err)
}
// create new sync group listings
values = make([]string, len(vs))
args = make(pgx.NamedArgs, len(vs))
for i, v := range vs {
values[i] = fmt.Sprintf("(@account_id_%[1]d, @sync_group_id_%[1]d, @shop_id_%[1]d, @listing_id_%[1]d, @order_index_%[1]d)", i)
args[fmt.Sprintf("account_id_%d", i)] = acctID
args[fmt.Sprintf("sync_group_id_%d", i)] = syncGroupID
args[fmt.Sprintf("shop_id_%d", i)] = v.Shop_id
args[fmt.Sprintf("listing_id_%d", i)] = v.Listing_id
args[fmt.Sprintf("order_index_%d", i)] = v.Order_index
}
_, err = tx.Exec(
ctx,
fmt.Sprintf(
`
INSERT INTO
%s (
account_id,
sync_group_id,
shop_id,
listing_id,
order_index
)
VALUES
%s
`,
in.syncGroupListingsTable,
strings.Join(values, ", "),
),
args,
)
if err != nil {
return fmt.Errorf("failed to perform query inserting new sync group listings: %w", err)
}
}
// there must be at least two listings per sync group
if listingCount < 2 {
return fmt.Errorf("%w: invalid number of listings for sync group: %d: minimum of two required", consts.ErrConflict, listingCount)
}
// delete the sync group editing record
// this will cascade the editing listing records
rows, err = tx.Query(
ctx,
`
DELETE FROM
mock.sync_group_editing
WHERE
account_id = @account_id
RETURNING
sync_group_id
`,
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query deleting editing sync group: %w", err)
}
if syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan row: %w", err)
}
return nil
})
return syncGroupID, err
}
func (db *Store) CancelEditingOfMockSyncGroupForAccount(ctx context.Context, acctID int64) (prevSyncGroupID int64, prevSyncGroupExists bool, err error) {
rows, err := db.db.Query(
ctx,
@@ -678,6 +891,156 @@ func (db *Store) CancelEditingOfMockSyncGroupForAccount(ctx context.Context, acc
return prevSyncGroupID, true, nil
}
func (db *Store) GetMockSyncGroupEditingListings(ctx context.Context, acctID int64) ([]ListingWithOrderIndex, error) {
var (
listings []ListingWithOrderIndex
allOrderIndexes []int
)
//orderIndexPerListing := make(map[Platform]map[string]int)
err := db.beginReadonlyTxn(ctx, func(tx pgx.Tx) error {
// get the mock sync group editing listings from each table
for _, in := range getAllMockShopSchemaInfos() {
rows, err := tx.Query(
ctx,
fmt.Sprintf(
`
SELECT
eli.shop_id,
eli.listing_id,
eli.order_index,
li.name,
li.count,
li.sku,
li.description,
sh.name as shop_name
FROM
%s AS eli
LEFT JOIN
%s AS li
USING
(account_id, shop_id, listing_id)
JOIN
%s AS sh
USING
(account_id, shop_id)
WHERE
account_id = @account_id
`,
in.syncGroupEditingListingTable,
in.listingsTable,
in.shopTable,
),
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query to get mock sync group editing listings: %w", err)
}
vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
Shop_id string
Listing_id pgtype.Text
Order_index int
Name pgtype.Text
Count pgtype.Int8
Sku pgtype.Text
Description pgtype.Text
Shop_name string
}])
if err != nil {
return fmt.Errorf("failed to scan rows: %w", err)
}
for _, v := range vs {
listings = append(listings, ListingWithOrderIndex{
Listing: Listing{
AccountShopListingIDs: NewAccountIDs(acctID).
ShopID(in.platform, v.Shop_id).
ListingID(v.Listing_id.String),
ShopName: v.Shop_name,
SKU: v.Sku.String,
Name: v.Name.String,
Description: v.Description.String,
Count: v.Count.Int64,
},
OrderIndex: v.Order_index,
})
}
}
// loading the empty indexes, too
rows, err := tx.Query(
ctx,
`
SELECT
order_index
FROM
mock.sync_group_editing_listing_order_indexes
WHERE
account_id = @account_id
`,
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
if allOrderIndexes, err = pgx.CollectRows(rows, pgx.RowTo[int]); err != nil {
return fmt.Errorf("failed to scan rows: %w", err)
}
return nil
})
slices.SortFunc(listings, func(a, b ListingWithOrderIndex) int {
return a.OrderIndex - b.OrderIndex
})
indexPerOrderIndex := make(map[int]int)
for i, v := range listings {
indexPerOrderIndex[v.OrderIndex] = i
}
for _, i := range allOrderIndexes {
if _, ok := indexPerOrderIndex[i]; ok {
continue
}
for j := i - 1; j >= 0; j -= 1 {
index, ok := indexPerOrderIndex[j]
if !ok {
continue
}
// insert the new empty listing into the list
listings = slices.Insert(listings, index+1, ListingWithOrderIndex{
Listing: Listing{
AccountShopListingIDs: NewAccountIDs(acctID).
ShopID("", "").
ListingID(""),
},
OrderIndex: i,
})
indexPerOrderIndex[i] = index + 1
for k, v := range indexPerOrderIndex {
if k > i {
indexPerOrderIndex[k] = v + 1
}
}
break
}
}
return listings, err
}
func (db *Store) MockSyncGroupIsBeingEdited(ctx context.Context, acctID, syncGroupID int64) (bool, error) {
rows, err := db.db.Query(
ctx,
@@ -709,6 +1072,395 @@ func (db *Store) MockSyncGroupIsBeingEdited(ctx context.Context, acctID, syncGro
return true, nil
}
func (db *Store) GetMockSyncGroupEditingListing(ctx context.Context, acctID, syncGroupID, orderIndex int64) (*Listing, error) {
var listing *Listing
err := db.beginReadonlyTxn(ctx, func(tx pgx.Tx) error {
// assert the sync group is being edited
rows, err := tx.Query(
ctx,
`
SELECT
true
FROM
mock.sync_group_editing
WHERE
account_id = @account_id
AND sync_group_id = @sync_group_id
`,
pgx.NamedArgs{
"account_id": acctID,
"sync_group_id": syncGroupID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan rows: %w", err)
}
// find the editing listing
for _, in := range getAllMockShopSchemaInfos() {
rows, err := tx.Query(
ctx,
fmt.Sprintf(
`
SELECT
eli.shop_id,
eli.listing_id,
eli.order_index,
li.name,
li.count,
li.sku,
li.description,
sh.name as shop_name
FROM
%s AS eli
JOIN
%s AS li
USING
(account_id, shop_id, listing_id)
JOIN
%s AS sh
USING
(account_id, shop_id)
WHERE
account_id = @account_id
AND eli.order_index = @order_index
`,
in.syncGroupEditingListingTable,
in.listingsTable,
in.shopTable,
),
pgx.NamedArgs{
"account_id": acctID,
"order_index": orderIndex,
},
)
if err != nil {
return fmt.Errorf("failed to perform query to get mock sync group editing listings: %w", err)
}
v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct {
Shop_id string
Listing_id string
Order_index int
Name string
Count int64
Sku string
Description string
Shop_name string
}])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
continue
}
return fmt.Errorf("failed to scan rows: %w", err)
}
listing = &Listing{
AccountShopListingIDs: NewAccountIDs(acctID).
ShopID(in.platform, v.Shop_id).
ListingID(v.Listing_id),
ShopName: v.Shop_name,
SKU: v.Sku,
Name: v.Name,
Description: v.Description,
Count: v.Count,
}
return nil
}
return consts.ErrNotFound
})
return listing, err
}
func (db *Store) AddListingToMockSyncGroupBeingEdited(ctx context.Context, acctID int64) (syncGroupID int64, orderIndex int, err error) {
err = db.beginTxn(ctx, func(tx pgx.Tx) error {
// assert the sync group is being edited
rows, err := tx.Query(
ctx,
`
SELECT
sync_group_id
FROM
mock.sync_group_editing
WHERE
account_id = @account_id
`,
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
syncGroupID, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan rows: %w", err)
}
rows, err = tx.Query(
ctx,
`
WITH new_order_index AS (
SELECT
COALESCE(MAX(order_index)+1, 0) AS order_index
FROM
mock.sync_group_editing_listing_order_indexes
)
INSERT INTO
mock.sync_group_editing_listing_order_indexes (
account_id,
order_index
)
SELECT
@account_id,
new_order_index.order_index
FROM
new_order_index
RETURNING
order_index
`,
pgx.NamedArgs{
"account_id": acctID,
},
)
if err != nil {
return fmt.Errorf("failed to insert new order index row: %w", err)
}
orderIndex, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int])
if err != nil {
return fmt.Errorf("failed to scan rows: %w", err)
}
return nil
})
return syncGroupID, orderIndex, err
}
func (db *Store) SetShopInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID, syncGroupID int64, orderIndex int, platform Platform, shopID string) error {
in, ok := getMockShopSchemaInfo(platform)
if !ok {
return consts.ErrNotFound
}
return db.beginTxn(ctx, func(tx pgx.Tx) error {
// assert the sync group is being edited
rows, err := tx.Query(
ctx,
`
SELECT
true
FROM
mock.sync_group_editing
WHERE
account_id = @account_id
AND sync_group_id = @sync_group_id
`,
pgx.NamedArgs{
"account_id": acctID,
"sync_group_id": syncGroupID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan rows: %w", err)
}
// delete old listing: this will delete the store-specific listing row implicitly without the lookup
tags, err := tx.Exec(
ctx,
`
DELETE FROM
mock.sync_group_editing_listing_order_indexes
WHERE
account_id = @account_id
AND order_index = @order_index
`,
pgx.NamedArgs{
"account_id": acctID,
"order_index": orderIndex,
},
)
if err != nil {
return fmt.Errorf("failed to deleted order index record: %w", err)
}
if tags.RowsAffected() == 0 {
return consts.ErrNotFound
}
// create new listing
_, err = tx.Exec(
ctx,
fmt.Sprintf(
`
WITH new_order_index_row(account_id, order_index) AS (
INSERT INTO
mock.sync_group_editing_listing_order_indexes (
account_id,
order_index
)
VALUES (
@account_id,
@order_index
)
RETURNING
account_id,
order_index
)
INSERT INTO
%s AS eli (
account_id,
order_index,
shop_id
)
SELECT
account_id,
order_index,
@shop_id as shop_id
FROM
new_order_index_row
`,
in.syncGroupEditingListingTable,
),
pgx.NamedArgs{
"account_id": acctID,
"order_index": orderIndex,
"shop_id": shopID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query to insert mock sync group editing listings: %w", err)
}
return nil
})
}
func (db *Store) SetListingInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID, syncGroupID int64, orderIndex int, listingID string) error {
return db.beginTxn(ctx, func(tx pgx.Tx) error {
// assert the sync group is being edited
rows, err := tx.Query(
ctx,
`
SELECT
true
FROM
mock.sync_group_editing
WHERE
account_id = @account_id
AND sync_group_id = @sync_group_id
`,
pgx.NamedArgs{
"account_id": acctID,
"sync_group_id": syncGroupID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return consts.ErrNotFound
}
return fmt.Errorf("failed to scan rows: %w", err)
}
// update the editing listing
for _, in := range getAllMockShopSchemaInfos() {
tag, err := tx.Exec(
ctx,
fmt.Sprintf(
`
UPDATE
%s AS eli
SET
listing_id = @listing_id
WHERE
account_id = @account_id
AND order_index = @order_index
`,
in.syncGroupEditingListingTable,
),
pgx.NamedArgs{
"account_id": acctID,
"order_index": orderIndex,
"listing_id": listingID,
},
)
if err != nil {
return fmt.Errorf("failed to perform query to update mock sync group editing listings: %w", err)
}
switch n := tag.RowsAffected(); n {
case 0:
continue
case 1:
return nil
default:
return fmt.Errorf("unexpected number of rows affected: %d", n)
}
return nil
}
return consts.ErrNotFound
})
}
func (db *Store) DeleteListingInListingInMockSyncGroupBeingEdited(ctx context.Context, acctID int64, orderIndex int) error {
tag, err := db.db.Exec(
ctx,
`
DELETE FROM
mock.sync_group_editing_listing_order_indexes
WHERE
account_id = @account_id
AND order_index = @order_index
`,
pgx.NamedArgs{
"account_id": acctID,
"order_index": orderIndex,
},
)
if err != nil {
return fmt.Errorf("failed to perform query: %w", err)
}
if tag.RowsAffected() == 0 {
return consts.ErrNotFound
}
return nil
}
// additional context
func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) {
@@ -721,3 +1473,11 @@ func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (
}
return nil, err
}
func (db *Store) beginReadonlyTxn(ctx context.Context, cb func(pgx.Tx) error) error {
return pgx.BeginTxFunc(ctx, db.db, pgx.TxOptions{AccessMode: pgx.ReadOnly}, cb)
}
func (db *Store) beginTxn(ctx context.Context, cb func(pgx.Tx) error) error {
return pgx.BeginFunc(ctx, db.db, cb)
}
+20
View File
@@ -69,14 +69,34 @@ func (v_ctx *StoreWithContext) StartEditingMockSyncGroup(acctID int64, syncGroup
return v_ctx.Store.StartEditingMockSyncGroup(v_ctx.ctx, acctID, syncGroupID)
}
func (v_ctx *StoreWithContext) CancelEditingOfMockSyncGroupForAccount(acctID int64) (int64, bool, error) {
return v_ctx.Store.CancelEditingOfMockSyncGroupForAccount(v_ctx.ctx, acctID)
}
func (v_ctx *StoreWithContext) GetMockSyncGroupEditingListings(acctID int64) ([]ListingWithOrderIndex, error) {
return v_ctx.Store.GetMockSyncGroupEditingListings(v_ctx.ctx, acctID)
}
func (v_ctx *StoreWithContext) MockSyncGroupIsBeingEdited(acctID int64, syncGroupID int64) (bool, error) {
return v_ctx.Store.MockSyncGroupIsBeingEdited(v_ctx.ctx, acctID, syncGroupID)
}
func (v_ctx *StoreWithContext) GetMockSyncGroupEditingListing(acctID int64, syncGroupID int64, orderIndex int64) (*Listing, error) {
return v_ctx.Store.GetMockSyncGroupEditingListing(v_ctx.ctx, acctID, syncGroupID, orderIndex)
}
func (v_ctx *StoreWithContext) SetShopInListingInMockSyncGroupBeingEdited(acctID int64, syncGroupID int64, orderIndex int, platform Platform, shopID string) error {
return v_ctx.Store.SetShopInListingInMockSyncGroupBeingEdited(v_ctx.ctx, acctID, syncGroupID, orderIndex, platform, shopID)
}
func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) {
return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID)
}
func (v_ctx *StoreWithContext) beginReadonlyTxn(cb func(pgx.Tx) error) error {
return v_ctx.Store.beginReadonlyTxn(v_ctx.ctx, cb)
}
func (v_ctx *StoreWithContext) CreateMockShop(acctID int64, platform Platform, name string) (uuid.UUID, error) {
return v_ctx.Store.CreateMockShop(v_ctx.ctx, acctID, platform, name)
}
+119 -2
View File
@@ -58,8 +58,17 @@ func Routes(
mockSyncGroups := syncGroups.Group("/mock")
mockSyncGroups.POST("", pub.Publish("/:acctID/inventory/sync-groups/mock"), response.Handler(as.saveNewMockSyncGroup))
mockSyncGroups.DELETE("/:syncGroupID", pub.Publish("/:acctID/inventory/sync-groups/mock"), response.Handler(as.deleteMockSyncGroup))
mockSyncGroups.PUT("/editing", pub.Publish("/:acctID/inventory/sync-groups/mock/editing"), response.Handler(as.selectMockSyncGroupForEditing))
mockSyncGroups.DELETE("/editing", pub.Publish("/:acctID/inventory/sync-groups/mock/editing"), response.Handler(as.cancelMockSyncGroupEdits))
setMockSyncGroupForEditingEndpoints := mockSyncGroups.Group("/editing", pub.Publish("/:acctID/inventory/sync-groups/mock/editing"))
setMockSyncGroupForEditingEndpoints.PUT("", response.Handler(as.selectMockSyncGroupForEditing))
setMockSyncGroupForEditingEndpoints.POST("", response.Handler(as.saveMockSyncGroupBeingEdited))
setMockSyncGroupForEditingEndpoints.DELETE("", response.Handler(as.cancelMockSyncGroupEdits))
setMockSyncGroupForEditingEndpoints.POST("/listings", response.Handler(as.addListingToMockSyncGroupBeingEdited))
mockSyncGroupBeingEdited := mockSyncGroups.Group("/:syncGroupID/editing", pub.Publish("/:acctID/inventory/sync-groups/mock/:syncGroupID/editing"))
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/shop", response.Handler(as.setShopInListingInMockSyncGroupBeingEdited))
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/listing", response.Handler(as.setListingInListingInMockSyncGroupBeingEdited))
mockSyncGroupBeingEdited.DELETE("/listings/:orderIndex", response.Handler(as.deleteListingInListingInMockSyncGroupBeingEdited))
mockDraftListings := mockSyncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/mock/draft/listings"))
mockDraftListings.POST("", response.Handler(as.createMockSyncGroupListingDraft))
@@ -571,6 +580,31 @@ func (s *accountSubrouter) selectMockSyncGroupForEditing(c *gin.Context) (respon
return response.StatusOK(), nil
}
// POST /:acctID/inventory/sync-groups/mock/editing
func (s *accountSubrouter) saveMockSyncGroupBeingEdited(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
syncGroupID, err := s.accts.SaveMockSyncGroupBeingEdited(ctx, acctID)
if err != nil {
return nil, response.Errorf("failed to save mock sync group being edited: %w", response.ErrorFromConstant(err))
}
// emit events indicating sync groups being edited and no longer being edited
if err := s.pub.Push(
c,
acctID,
fmt.Sprintf("accounts_%d_inventory_sync-groups_mock_%d", acctID, syncGroupID),
fmt.Sprintf("accounts_%d_inventory_sync-groups_mock_%d_editing", acctID, syncGroupID),
); err != nil {
s.log.Errorf("failed to publish mock sync group editing events: %v", err)
}
return response.StatusOK(), nil
}
// DELETE /:acctID/inventory/sync-groups/mock/editing
func (s *accountSubrouter) cancelMockSyncGroupEdits(c *gin.Context) (response.Response, error) {
r := c.Request
@@ -593,6 +627,89 @@ func (s *accountSubrouter) cancelMockSyncGroupEdits(c *gin.Context) (response.Re
return response.StatusOK(), nil
}
// POST /:acctID/inventory/sync-groups/mock/editing/listings
func (s *accountSubrouter) addListingToMockSyncGroupBeingEdited(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
acctID := auth.GetIdentity(ctx).Account.AccountID
if _, _, err := s.accts.AddListingToMockSyncGroupBeingEdited(ctx, acctID); err != nil {
return nil, response.Errorf("failed to add listing to mock sync group being edited: %w", response.ErrorFromConstant(err))
}
return response.StatusCreated(), nil
}
// PUT /:acctID/inventory/sync-groups/mock/editing/listings/:orderIndex/shop
func (s *accountSubrouter) setShopInListingInMockSyncGroupBeingEdited(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
syncGroupID int64
orderIndex int
platform accounts.Platform
shopID string
)
if err := param.Path("syncGroupID", param.Int64(&syncGroupID)).
Path("orderIndex", param.Int(&orderIndex)).
Form("platform", param.Platform(&platform)).
Form("shop-id", param.Text(&shopID)).
Unmarshal(c); err != nil {
return nil, err
}
if err := s.accts.SetShopInListingInMockSyncGroupBeingEdited(c, acctID, syncGroupID, orderIndex, platform, shopID); err != nil {
return nil, response.Errorf("failed to set shop in listing in mock sync group being edited: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// PUT /:acctID/inventory/sync-groups/mock/editing/listings/:orderIndex/listing
func (s *accountSubrouter) setListingInListingInMockSyncGroupBeingEdited(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
syncGroupID int64
orderIndex int
listingID string
)
if err := param.Path("syncGroupID", param.Int64(&syncGroupID)).
Path("orderIndex", param.Int(&orderIndex)).
Form("listing-id", param.Text(&listingID)).
Unmarshal(c); err != nil {
return nil, err
}
if err := s.accts.SetListingInListingInMockSyncGroupBeingEdited(c, acctID, syncGroupID, orderIndex, listingID); err != nil {
return nil, response.Errorf("failed to set listing in listing in mock sync group being edited: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
// DELETE /:acctID/inventory/sync-groups/mock/editing/listings/:orderIndex
func (s *accountSubrouter) deleteListingInListingInMockSyncGroupBeingEdited(c *gin.Context) (response.Response, error) {
acctID := auth.GetIdentity(c).Account.AccountID
var (
orderIndex int
)
if err := param.Path("orderIndex", param.Int(&orderIndex)).
Unmarshal(c); err != nil {
return nil, err
}
if err := s.accts.DeleteListingInListingInMockSyncGroupBeingEdited(c, acctID, orderIndex); err != nil {
return nil, response.Errorf("failed to delete listing in mock sync group being edited: %w", response.ErrorFromConstant(err))
}
return response.StatusOK(), nil
}
func lowerSnakeCase(s accounts.Platform) string {
return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_"))
}
-23
View File
@@ -197,29 +197,6 @@
.bottom-0 {
bottom: calc(var(--spacing) * 0);
}
.grid-table-\[1fr_1fr_1fr_1fr_1fr_1fr\] {
&:is(table) {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
& > :is(thead, tbody, tfoot) {
grid-column-start: 1;
grid-column-end: -1;
display: grid;
grid-template-columns: subgrid;
& > tr {
grid-column-start: 1;
grid-column-end: -1;
display: grid;
grid-template-columns: subgrid;
& > th, & > td {
grid-column: span 1;
align-content: center;
text-align: center;
}
}
}
}
}
.grid-table-\[1fr_1fr_1fr_1fr_1fr_1fr_1fr\] {
&:is(table) {
display: grid;
@@ -29,13 +29,12 @@
on setListing(listing)
call setFilledOut(true)
on resetListing
call setFilledOut(false)
"
>
<td class="text-nowrap">
{{/* TODO: filter out shops that are already selected */}}
{{/* filter out shops that are already selected */}}
{{- $shops := .Accounts.GetMockShops $acctID -}}
<select
class="min-w-fit cursor-pointer"
@@ -1,211 +0,0 @@
{{/* .SyncGroup: optional for performance */}}
{{- $syncGroupID := .PathParams.syncGroupID }}
{{- $acctID := .PathParams.acctID }}
{{- $isBeingEditing := .Accounts.MockSyncGroupIsBeingEdited $acctID $syncGroupID }}
{{- $open := or $isBeingEditing (and .Request (eq (.Request.URL.Query.Get "open") "true")) }}
{{- $grp := .SyncGroup }}
{{- if not $grp }}
{{- $grp = .Accounts.GetMockSyncGroup $acctID $syncGroupID }}
{{- end }}
{{- $id := (printf "sync-group-%d" $syncGroupID) }}
<li
id="{{$id}}"
class="mx-[1em]"
hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/mock/%d/list-item" $acctID $syncGroupID }}"
hx-trigger="{{ printf "sse:accounts_%d_inventory_sync-groups_mock_%d" $acctID $syncGroupID }}"
hx-swap="morph:outerHTML"
hx-select="#{{$id}}"
hx-vals="js:{open: this.getElementsByTagName('details')[0].open}"
>
{{- define "sync-group-accordion-header" }}
<h3 class="text-center">
{{ .Group.Name }}
</h3>
{{- end }}
{{- define "sync-group-accordion-editing-header" }}
<div class="flex flex-col items-center">
<h3 class="text-center">
{{ .Group.Name }}
</h3>
<h5>
Editing
</h5>
</div>
{{- end }}
{{- define "sync-group-accordion-static-body" }}
{{- $acctID := .Identity.Account.AccountID }}
{{- $syncGroupID := .Group.SyncGroupID }}
<table id="sync-group-data-{{$syncGroupID}}">
<thead>
<tr>
<th>
Shop
</th>
<th>
Platform
</th>
<th>
Name
</th>
<th>
SKU
</th>
<th>
Description
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{- $dot := . }}
{{- range $listing := .Group.Listings }}
<tr>
{{- $val := $dot.Accounts.GetMockListing $acctID $listing.Platform $listing.ShopID $listing.ListingID }}
<td class="text-nowrap">
{{ $val.ShopName }}
</td>
<td>
{{ $val.Platform }}
</td>
<td class="text-nowrap">
{{ $val.Name }}
</td>
<td class="text-nowrap">
{{ $val.SKU }}
</td>
<td class="text-nowrap">
{{ $val.Description }}
</td>
<td>
{{ $val.Count }}
</td>
</tr>
{{- end }}
</tbody>
</table>
<div
id="sync-group-{{$syncGroupID}}-buttons"
class="flex justify-center flex-grow-[1] gap-[0.5em]"
>
{{- component "button"
"Class" "mt-[1em]"
"Text" "Edit"
"HXPut" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing" $acctID)
"HXVals" (printf `js:{"syncGroupID":%d}` $syncGroupID)
"HXSwap" "none"
}}
{{- component "button"
"Class" "mt-[1em]"
"Text" "Delete"
"HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/%d" $acctID $syncGroupID)
"HXSwap" "none"
}}
</div>
{{- end }}
{{- define "sync-group-accordion-form-body" }}
{{- $acctID := .Identity.Account.AccountID }}
{{- $syncGroupID := .Group.SyncGroupID }}
<form>
<table id="sync-group-{{$syncGroupID}}-form">
<thead>
<tr>
<th>
Shop
</th>
<th>
Platform
</th>
<th>
Name
</th>
<th>
SKU
</th>
<th>
Description
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{- $dot := . }}
{{- range $listing := .Group.Listings }}
<tr>
{{- $val := $dot.Accounts.GetMockListing $acctID $listing.Platform $listing.ShopID $listing.ListingID }}
<td>
{{ $val.ShopName }}
</td>
<td>
{{ $val.Platform }}
</td>
<td>
{{ $val.Name }}
</td>
<td>
{{ $val.SKU }}
</td>
<td>
{{ $val.Description }}
</td>
<td>
{{ $val.Count }}
</td>
</tr>
{{- end }}
</tbody>
</table>
<div
id="sync-group-{{$syncGroupID}}-editing-buttons"
class="flex justify-center flex-grow-[1] gap-[0.5em]"
>
{{/* TODO: implement saving */}}
{{- component "button"
"Class" "mt-[1em]"
"Text" "Save"
"HXSwap" "none"
}}
{{- component "button"
"Class" "mt-[1em]"
"Text" "Cancel"
"HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing" $acctID)
"HXSwap" "none"
}}
</div>
</form>
{{- end }}
{{- $accordionHeader := "sync-group-accordion-header" }}
{{- $accordionBody := "sync-group-accordion-static-body" }}
{{- $summaryClass := "" }}
{{- if $isBeingEditing }}
{{- $accordionBody = "sync-group-accordion-form-body" }}
{{- $accordionHeader = "sync-group-accordion-editing-header" }}
{{- $summaryClass = "bg-accent-secondary" }}
{{- end }}
{{ component "accordion"
"Name" "sync-group-page"
"Open" $open
"GroupName" "sync-group"
"Group" $grp
"Smooth" false
"SummaryClass" $summaryClass
"#accordion-header" $accordionHeader
"#accordion-body" $accordionBody
}}
</li>
@@ -0,0 +1,117 @@
{{- $acctID := .Identity.Account.AccountID }}
{{- $syncGroupID := .PathParams.syncGroupID }}
<form class="w-full">
<table
id="sync-group-{{$syncGroupID}}-form"
class="w-full overflow-x-auto"
hx-get="/ui/accounts/{{$acctID}}/inventory/sync-groups/mock/{{$syncGroupID}}/list-item/edit-form"
hx-trigger="sse:{{ printf "accounts_%d_inventory_sync-groups_mock_%d_editing" $acctID $syncGroupID }},sse:{{ printf "accounts_%d_inventory_sync-groups_mock_editing" $acctID }}"
hx-select="#sync-group-{{$syncGroupID}}-form"
hx-swap="morph"
>
<thead>
<tr>
<th>
Shop
</th>
<th>
Platform
</th>
<th>
Listing
</th>
<th>
SKU
</th>
<th>
Description
</th>
<th>
Count
</th>
<th>
</th>
</tr>
</thead>
<tbody>
{{- $dot := . }}
{{- $editListings := .Accounts.GetMockSyncGroupEditingListings $acctID }}
{{- range $listing := $editListings }}
{{- component (printf "accounts/%d/inventory/sync-groups/mock/%d/list-item/edit-form/edit-form-listing/%d" $acctID $syncGroupID $listing.OrderIndex)
"Listing" $listing
"Listings" $editListings
}}
{{- end }}
</tbody>
{{- if gt (len $editListings) 5 }}
<tfoot class="sticky bottom-0 outline-1 outline-(--table-tfoot)">
<tr>
<th>
Shop
</th>
<th>
Platform
</th>
<th>
Listing
</th>
<th>
SKU
</th>
<th>
Description
</th>
<th>
Count
</th>
<th>
</th>
</tr>
</tfoot>
{{- end }}
</table>
<div
id="sync-group-{{$syncGroupID}}-editing-buttons"
class="flex justify-center flex-grow-[1] gap-[0.5em]"
>
{{/* TODO: implement */}}
{{- component "button"
"ID" "add-mock-sync-group-listing-button"
"Class" "mt-[1em]"
"Text" "Add Listing"
"HXPost" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing/listings" $acctID)
"HXSwap" "none"
}}
{{- component "button"
"ID" "save-mock-sync-group-button"
"Class" "mt-[1em]"
"Text" "Save"
"HXPost" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing" $acctID)
"HXSwap" "none"
}}
{{- component "button"
"Class" "mt-[1em]"
"Text" "Cancel"
"HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing" $acctID)
"HXSwap" "none"
}}
{{/*
<div
hidden
hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/mock/draft/table/save-sync-group-button" $acctID }}"
hx-trigger="sse:{{ printf "accounts_%d_inventory_sync-groups_mock_%d_editing_listings" $acctID $syncGroupID }}"
hx-target="#save-mock-sync-group-button"
hx-swap="outerHTML"
>
</div>
*/}}
</div>
</form>
@@ -0,0 +1,140 @@
{{/* .Listing */}}
{{/* optional props: .Listings = .Accounts.GetMockSyncGroupListingDrafts */}}
{{- $acctID := .PathParams.acctID }}
{{- $syncGroupID := .PathParams.syncGroupID }}
{{- $orderIndex := .PathParams.orderIndex }}
{{- $entry := .Listing }}
{{- if not $entry }}
{{- $entry = .Accounts.GetMockSyncGroupEditingListing $acctID $syncGroupID $orderIndex }}
{{- end }}
{{- $selectedShopPlatform := $entry.Platform }}
{{- $selectedShopID := $entry.ShopID }}
{{- $selectedListingID := $entry.ListingID }}
{{- $listings := .Listings }}
{{- if not $listings }}
{{- $listings := .Accounts.GetMockSyncGroupEditingListings $acctID }}
{{- end }}
{{- $listing := .Listing }}
<tr id="{{$syncGroupID}}-editing-{{$orderIndex}}">
<td class="text-nowrap">
{{/* filter out shops that are already selected */}}
{{/* $listing.ShopName */}}
{{- $shops := .Accounts.GetMockShops $acctID -}}
<select
class="min-w-fit cursor-pointer"
hx-put="/api/accounts/{{$acctID}}/inventory/sync-groups/mock/{{$syncGroupID}}/editing/listings/{{$orderIndex}}/shop"
hx-vals='js:{
platform: event.target.value.replace(/-[^ ]*/, ""),
"shop-id": event.target.value.replace(/^[a-zA-Z_]*-/, "")
}'
hx-swap="none"
{{/* TODO: drop events if not used */}}
_="
on change
send resetListing() to closest <tr/>
"
>
<option disabled {{- if not $selectedShopID }} selected{{- end }}>- Shops -</option>
{{- range $shop := $shops }}
{{- $shopSelectedInOtherListing := false }}
{{- if not (eq $shop.ShopID $selectedShopID ) }}
{{- range $listing := $listings }}
{{- if eq $listing.ShopID $shop.ShopID }}
{{- $shopSelectedInOtherListing = true }}
{{- break }}
{{- end }}
{{- end }}
{{- if $shopSelectedInOtherListing }}
{{- continue }}
{{- end }}
{{- end }}
{{- $isSelectedShop := and
(eq $selectedShopPlatform $shop.Platform)
(eq $selectedShopID $shop.ShopID)
}}
<option
id="{{$shop.Platform}}-{{$shop.ShopID}}"
value="{{$shop.Platform}}-{{$shop.ShopID}}"
{{- if $isSelectedShop }} selected{{- end }}
>
{{ $shop.Name }}
</option>
{{- end }}
</select>
</td>
<td>
{{- if $entry.Platform }}{{ $entry.Platform }}{{ else }}-{{ end }}
</td>
<td>
{{/* $listing.Name */}}
{{- $selectedListing := "" }}
{{- if $selectedShopID }}
{{- $shopListings := .Accounts.GetMockListingsForShop $acctID $selectedShopPlatform $selectedShopID }}
{{- if $shopListings }}
<select
class="min-w-fit cursor-pointer"
hx-put="/api/accounts/{{$acctID}}/inventory/sync-groups/mock/{{$syncGroupID}}/editing/listings/{{$orderIndex}}/listing"
hx-vals='js:{
"listing-id": event.target.value,
}'
hx-swap="none"
_="
on input
set dataset to (my selectedOptions)[0].dataset
send setListing(listing: dataset) to the closest <tr/>
"
>
<option disabled {{if not $selectedListingID}}selected{{end}}>- Listings -</option>
{{- range $i, $shopListing := $shopListings }}
{{- if eq $selectedListingID $shopListing.ListingID }}
{{ $selectedListing = $shopListing }}
{{- end }}
<option
value="{{$shopListing.ListingID}}"
{{if eq $selectedListingID $shopListing.ListingID}}selected{{end}}
data-name="{{ $shopListing.Name }}"
data-sku="{{ $shopListing.SKU }}"
data-description="{{ $shopListing.Description }}"
data-count="{{ $shopListing.Count }}"
>
{{ $shopListing.Name }}
</option>
{{- end }}
</select>
{{- else }}
no listings found
{{- end }}
{{- else }}
-
{{- end }}
</td>
<td>
{{ $listing.SKU }}
</td>
<td>
{{ $listing.Description }}
</td>
<td>
{{ $listing.Count }}
</td>
<td>
{{ component "button"
"HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/%d/editing/listings/%d" $acctID $syncGroupID $orderIndex)
"HXTarget" "#accounts-acct-id-inventory-sync-groups-mock-draft-table"
"HXSwap" "none"
"Text" "Remove"
}}
</td>
</tr>
@@ -0,0 +1,76 @@
{{/* .SyncGroup: optional for performance */}}
{{- $syncGroupID := .PathParams.syncGroupID }}
{{- $acctID := .PathParams.acctID }}
{{- $isBeingEditing := .Accounts.MockSyncGroupIsBeingEdited $acctID $syncGroupID }}
{{- $open := or $isBeingEditing (and .Request (eq (.Request.URL.Query.Get "open") "true")) }}
{{- $grp := .SyncGroup }}
{{- if not $grp }}
{{- $grp = .Accounts.GetMockSyncGroup $acctID $syncGroupID }}
{{- end }}
{{- $id := (printf "sync-group-%d" $syncGroupID) }}
<li
id="{{$id}}"
class="mx-[1em]"
hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/mock/%d/list-item" $acctID $syncGroupID }}"
hx-trigger="{{ printf "sse:accounts_%d_inventory_sync-groups_mock_%d" $acctID $syncGroupID }}"
hx-swap="morph:outerHTML"
hx-select="#{{$id}}"
hx-vals="js:{open: this.getElementsByTagName('details')[0].open}"
>
{{- define "sync-group-accordion-header" }}
<h3 class="text-center">
{{ .Group.Name }}
</h3>
{{- end }}
{{- define "sync-group-accordion-editing-header" }}
<div class="flex flex-col items-center">
<h3 class="text-center">
{{ .Group.Name }}
</h3>
<h5>
Editing
</h5>
</div>
{{- end }}
{{- define "sync-group-accordion-static-table" }}
{{- component (
printf "accounts/%d/inventory/sync-groups/mock/%d/list-item/static-table"
.Identity.Account.AccountID
.Group.SyncGroupID
)}}
{{- end }}
{{- define "sync-group-accordion-edit-form" }}
{{- component (
printf "accounts/%d/inventory/sync-groups/mock/%d/list-item/edit-form"
.Identity.Account.AccountID
.Group.SyncGroupID
)}}
{{- end }}
{{- $accordionHeader := "sync-group-accordion-header" }}
{{- $accordionBody := "sync-group-accordion-static-table" }}
{{- $summaryClass := "" }}
{{- if $isBeingEditing }}
{{- $accordionBody = "sync-group-accordion-edit-form" }}
{{- $accordionHeader = "sync-group-accordion-editing-header" }}
{{- $summaryClass = "bg-accent-secondary" }}
{{- end }}
{{ component "accordion"
"Name" "sync-group-page"
"Open" $open
"GroupName" "sync-group"
"Group" $grp
"Smooth" false
"SummaryClass" $summaryClass
"#accordion-header" $accordionHeader
"#accordion-body" $accordionBody
}}
</li>
@@ -0,0 +1,74 @@
{{/* .Group */}}
{{- $acctID := .Identity.Account.AccountID }}
{{- $syncGroupID := .PathParams.syncGroupID -}}
<table id="sync-group-data-{{$syncGroupID}}">
<thead>
<tr>
<th>
Shop
</th>
<th>
Platform
</th>
<th>
Name
</th>
<th>
SKU
</th>
<th>
Description
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{- $dot := . }}
{{- range $listing := .Group.Listings }}
<tr>
{{- $val := $dot.Accounts.GetMockListing $acctID $listing.Platform $listing.ShopID $listing.ListingID }}
<td class="text-nowrap">
{{ $val.ShopName }}
</td>
<td>
{{ $val.Platform }}
</td>
<td class="text-nowrap">
{{ $val.Name }}
</td>
<td class="text-nowrap">
{{ $val.SKU }}
</td>
<td class="text-nowrap">
{{ $val.Description }}
</td>
<td>
{{ $val.Count }}
</td>
</tr>
{{- end }}
</tbody>
</table>
<div
id="sync-group-{{$syncGroupID}}-buttons"
class="flex justify-center flex-grow-[1] gap-[0.5em]"
>
{{- component "button"
"Class" "mt-[1em]"
"Text" "Edit"
"HXPut" (printf "/api/accounts/%d/inventory/sync-groups/mock/editing" $acctID)
"HXVals" (printf `js:{"syncGroupID":%d}` $syncGroupID)
"HXSwap" "none"
}}
{{- component "button"
"Class" "mt-[1em]"
"Text" "Delete"
"HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/%d" $acctID $syncGroupID)
"HXSwap" "none"
}}
</div>