From 95ef64ce1857842b78a9f8991952f337315608a5 Mon Sep 17 00:00:00 2001 From: Angel Beltran Date: Tue, 10 Feb 2026 18:05:30 -0700 Subject: [PATCH] ability to delete mock sync groups --- .../000022_mock_sync_group_drafts.down.sql | 8 + .../000022_mock_sync_group_drafts.up.sql | 10 + ..._sync_group_listing_cascade_deletes.up.sql | 208 ++++++++++ domains/accounts/accounts.go | 90 +++++ domains/accounts/mocks.go | 14 + domains/accounts/store_with_context.go | 17 + domains/accounts/sync_groups.go | 367 ++++++++++++++++++ server/api/accounts/router.go | 48 +++ server/param/params.go | 14 + styles/index.css | 26 ++ .../draft/listings/{orderIndex.int}.html.tmpl | 6 +- .../mock/draft/table/index.html.tmpl | 10 +- .../table/save-sync-group-button.html.tmpl | 5 +- templates/components/button.html.tmpl | 2 +- .../{acctID.int64}/inventory/mock.html.tmpl | 151 ++++++- 15 files changed, 963 insertions(+), 13 deletions(-) create mode 100644 database_migrations/000022_mock_sync_group_drafts.down.sql create mode 100644 database_migrations/000022_mock_sync_group_drafts.up.sql create mode 100644 database_migrations/000023_mock_sync_group_listing_cascade_deletes.up.sql diff --git a/database_migrations/000022_mock_sync_group_drafts.down.sql b/database_migrations/000022_mock_sync_group_drafts.down.sql new file mode 100644 index 0000000..3c2368e --- /dev/null +++ b/database_migrations/000022_mock_sync_group_drafts.down.sql @@ -0,0 +1,8 @@ +BEGIN; + + +DROP TABLE mock.sync_group_drafts; + + +COMMIT; + diff --git a/database_migrations/000022_mock_sync_group_drafts.up.sql b/database_migrations/000022_mock_sync_group_drafts.up.sql new file mode 100644 index 0000000..aed88ca --- /dev/null +++ b/database_migrations/000022_mock_sync_group_drafts.up.sql @@ -0,0 +1,10 @@ +BEGIN; + + +CREATE TABLE mock.sync_group_drafts ( + account_id INTEGER PRIMARY KEY REFERENCES mock.accounts, + sync_group_id INTEGER REFERENCES mock.sync_groups +); + + +COMMIT; diff --git a/database_migrations/000023_mock_sync_group_listing_cascade_deletes.up.sql b/database_migrations/000023_mock_sync_group_listing_cascade_deletes.up.sql new file mode 100644 index 0000000..7162016 --- /dev/null +++ b/database_migrations/000023_mock_sync_group_listing_cascade_deletes.up.sql @@ -0,0 +1,208 @@ +BEGIN; + + +ALTER TABLE mock.shop_amazon_sync_group_listings + ADD CONSTRAINT shop_amazon_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_amazon_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_amazon_sync_group_listings + RENAME CONSTRAINT shop_amazon_sync_group_listings_tmp_key TO shop_amazon_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_amazon_sync_group_listings + ADD CONSTRAINT shop_amazon_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_amazon_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_amazon_sync_group_listings + RENAME CONSTRAINT shop_amazon_sync_group_listings_tmp_sync_group_id_fkey TO shop_amazon_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_big_cartel_sync_group_listings + ADD CONSTRAINT shop_big_cartel_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_big_cartel_sync_group_listi_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_big_cartel_sync_group_listings + RENAME CONSTRAINT shop_big_cartel_sync_group_listings_tmp_key TO shop_big_cartel_sync_group_listi_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_big_cartel_sync_group_listings + ADD CONSTRAINT shop_big_cartel_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_big_cartel_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_big_cartel_sync_group_listings + RENAME CONSTRAINT shop_big_cartel_sync_group_listings_tmp_sync_group_id_fkey TO shop_big_cartel_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_ebay_sync_group_listings + ADD CONSTRAINT shop_ebay_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_ebay_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_ebay_sync_group_listings + RENAME CONSTRAINT shop_ebay_sync_group_listings_tmp_key TO shop_ebay_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_ebay_sync_group_listings + ADD CONSTRAINT shop_ebay_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_ebay_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_ebay_sync_group_listings + RENAME CONSTRAINT shop_ebay_sync_group_listings_tmp_sync_group_id_fkey TO shop_ebay_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_ecwid_sync_group_listings + ADD CONSTRAINT shop_ecwid_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_ecwid_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_ecwid_sync_group_listings + RENAME CONSTRAINT shop_ecwid_sync_group_listings_tmp_key TO shop_ecwid_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_ecwid_sync_group_listings + ADD CONSTRAINT shop_ecwid_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_ecwid_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_ecwid_sync_group_listings + RENAME CONSTRAINT shop_ecwid_sync_group_listings_tmp_sync_group_id_fkey TO shop_ecwid_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_etsy_sync_group_listings + ADD CONSTRAINT shop_etsy_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_etsy_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_etsy_sync_group_listings + RENAME CONSTRAINT shop_etsy_sync_group_listings_tmp_key TO shop_etsy_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_etsy_sync_group_listings + ADD CONSTRAINT shop_etsy_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_etsy_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_etsy_sync_group_listings + RENAME CONSTRAINT shop_etsy_sync_group_listings_tmp_sync_group_id_fkey TO shop_etsy_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_shopify_sync_group_listings + ADD CONSTRAINT shop_shopify_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_shopify_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_shopify_sync_group_listings + RENAME CONSTRAINT shop_shopify_sync_group_listings_tmp_key TO shop_shopify_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_shopify_sync_group_listings + ADD CONSTRAINT shop_shopify_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_shopify_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_shopify_sync_group_listings + RENAME CONSTRAINT shop_shopify_sync_group_listings_tmp_sync_group_id_fkey TO shop_shopify_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_square_online_sync_group_listings + ADD CONSTRAINT shop_square_online_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_square_online_sync_group_li_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_square_online_sync_group_listings + RENAME CONSTRAINT shop_square_online_sync_group_listings_tmp_key TO shop_square_online_sync_group_li_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_square_online_sync_group_listings + ADD CONSTRAINT shop_square_online_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_square_online_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_square_online_sync_group_listings + RENAME CONSTRAINT shop_square_online_sync_group_listings_tmp_sync_group_id_fkey TO shop_square_online_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_squarespace_sync_group_listings + ADD CONSTRAINT shop_squarespace_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_squarespace_sync_group_list_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_squarespace_sync_group_listings + RENAME CONSTRAINT shop_squarespace_sync_group_listings_tmp_key TO shop_squarespace_sync_group_list_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_squarespace_sync_group_listings + ADD CONSTRAINT shop_squarespace_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_squarespace_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_squarespace_sync_group_listings + RENAME CONSTRAINT shop_squarespace_sync_group_listings_tmp_sync_group_id_fkey TO shop_squarespace_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_tiktok_sync_group_listings + ADD CONSTRAINT shop_tiktok_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_tiktok_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_tiktok_sync_group_listings + RENAME CONSTRAINT shop_tiktok_sync_group_listings_tmp_key TO shop_tiktok_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_tiktok_sync_group_listings + ADD CONSTRAINT shop_tiktok_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_tiktok_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_tiktok_sync_group_listings + RENAME CONSTRAINT shop_tiktok_sync_group_listings_tmp_sync_group_id_fkey TO shop_tiktok_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_walmart_marketplace_sync_group_listings + ADD CONSTRAINT shop_walmart_marketplace_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_walmart_marketplace_sync_gr_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_walmart_marketplace_sync_group_listings + RENAME CONSTRAINT shop_walmart_marketplace_sync_group_listings_tmp_key TO shop_walmart_marketplace_sync_gr_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_walmart_marketplace_sync_group_listings + ADD CONSTRAINT shop_walmart_marketplace_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_walmart_marketplace_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_walmart_marketplace_sync_group_listings + RENAME CONSTRAINT shop_walmart_marketplace_sync_group_listings_tmp_sync_group_id_fkey TO shop_walmart_marketplace_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_wix_sync_group_listings + ADD CONSTRAINT shop_wix_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_wix_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_wix_sync_group_listings + RENAME CONSTRAINT shop_wix_sync_group_listings_tmp_key TO shop_wix_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_wix_sync_group_listings + ADD CONSTRAINT shop_wix_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_wix_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_wix_sync_group_listings + RENAME CONSTRAINT shop_wix_sync_group_listings_tmp_sync_group_id_fkey TO shop_wix_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_woo_commerce_sync_group_listings + ADD CONSTRAINT shop_woo_commerce_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_woo_commerce_sync_group_lis_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_woo_commerce_sync_group_listings + RENAME CONSTRAINT shop_woo_commerce_sync_group_listings_tmp_key TO shop_woo_commerce_sync_group_lis_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_woo_commerce_sync_group_listings + ADD CONSTRAINT shop_woo_commerce_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_woo_commerce_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_woo_commerce_sync_group_listings + RENAME CONSTRAINT shop_woo_commerce_sync_group_listings_tmp_sync_group_id_fkey TO shop_woo_commerce_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.shop_zoho_sync_group_listings + ADD CONSTRAINT shop_zoho_sync_group_listings_tmp_key + FOREIGN KEY (sync_group_id, order_index) REFERENCES mock.sync_group_listing_order_indexes(sync_group_id, order_index) ON DELETE CASCADE, + DROP CONSTRAINT shop_zoho_sync_group_listings_sync_group_id_order_index_fkey; +ALTER TABLE mock.shop_zoho_sync_group_listings + RENAME CONSTRAINT shop_zoho_sync_group_listings_tmp_key TO shop_zoho_sync_group_listings_sync_group_id_order_index_fkey; + +ALTER TABLE mock.shop_zoho_sync_group_listings + ADD CONSTRAINT shop_zoho_sync_group_listings_tmp_sync_group_id_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT shop_zoho_sync_group_listings_sync_group_id_fkey; +ALTER TABLE mock.shop_zoho_sync_group_listings + RENAME CONSTRAINT shop_zoho_sync_group_listings_tmp_sync_group_id_fkey TO shop_zoho_sync_group_listings_sync_group_id_fkey; + + +ALTER TABLE mock.sync_group_listing_order_indexes + ADD CONSTRAINT tmp_fkey + FOREIGN KEY (sync_group_id) REFERENCES mock.sync_groups(sync_group_id) ON DELETE CASCADE, + DROP CONSTRAINT sync_group_listing_order_indexes_sync_group_id_fkey; +ALTER TABLE mock.sync_group_listing_order_indexes + RENAME CONSTRAINT tmp_fkey TO sync_group_listing_order_indexes_sync_group_id_fkey; + + + +COMMIT; diff --git a/domains/accounts/accounts.go b/domains/accounts/accounts.go index 7043fa8..bd666af 100644 --- a/domains/accounts/accounts.go +++ b/domains/accounts/accounts.go @@ -410,6 +410,96 @@ func (db *Store) GetMockListingsForShop(ctx context.Context, acctID int64, platf return listings, nil } +func (db *Store) GetMockListing(ctx context.Context, acctID int64, platform Platform, shopID, listingID string) (*Listing, error) { + in, ok := getMockShopSchemaInfo(platform) + if !ok { + return nil, fmt.Errorf("%w: unrecognized platform: %s", consts.ErrNotFound, platform) + } + + rows, err := db.db.Query( + ctx, + fmt.Sprintf( + ` + SELECT + name, + count, + sku, + description + FROM + %s + WHERE + account_id = @account_id + AND shop_id = @shop_id + AND listing_id = @listing_id + `, + in.listingsTable, + ), + pgx.NamedArgs{ + "account_id": acctID, + "shop_id": shopID, + "listing_id": listingID, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to perform query: %w", err) + } + + v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { + Name string + Count int64 + Sku string + Description string + }]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, consts.ErrNotFound + } + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + return &Listing{ + AccountShopListingIDs: AccountShopListingIDs{ + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + Platform: platform, + ShopID: shopID, + }, + ListingID: listingID, + }, + SKU: v.Sku, + Name: v.Name, + Description: v.Name, + Count: v.Count, + }, nil +} + +func (db *Store) DeleteMockSyncGroup(ctx context.Context, acctID, syncGroupID int64) error { + tag, err := db.db.Exec( + ctx, + ` + DELETE FROM + mock.sync_groups + 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 tag.RowsAffected() == 0 { + return consts.ErrNotFound + } + + return nil +} + // additional context func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) { diff --git a/domains/accounts/mocks.go b/domains/accounts/mocks.go index 99d33a3..7b201fc 100644 --- a/domains/accounts/mocks.go +++ b/domains/accounts/mocks.go @@ -526,6 +526,7 @@ type mockShopSchemaInfo struct { platform Platform shopTable string listingsTable string + syncGroupListingsTable string syncGroupListingDraftsTable string } @@ -545,78 +546,91 @@ func getAllMockShopSchemaInfos() []mockShopSchemaInfo { platform: Amazon, shopTable: "mock.shop_amazon", listingsTable: "mock.shop_amazon_listings", + syncGroupListingsTable: "mock.shop_amazon_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_amazon_sync_group_listing_drafts", }, { platform: BigCartel, shopTable: "mock.shop_big_cartel", listingsTable: "mock.shop_big_cartel_listings", + syncGroupListingsTable: "mock.shop_big_cartel_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_big_cartel_sync_group_listing_drafts", }, { platform: Ebay, shopTable: "mock.shop_ebay", listingsTable: "mock.shop_ebay_listings", + syncGroupListingsTable: "mock.shop_ebay_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_ebay_sync_group_listing_drafts", }, { platform: Ecwid, shopTable: "mock.shop_ecwid", listingsTable: "mock.shop_ecwid_listings", + syncGroupListingsTable: "mock.shop_ecwid_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_ecwid_sync_group_listing_drafts", }, { platform: Etsy, shopTable: "mock.shop_etsy", listingsTable: "mock.shop_etsy_listings", + syncGroupListingsTable: "mock.shop_etsy_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_etsy_sync_group_listing_drafts", }, { platform: Shopify, shopTable: "mock.shop_shopify", listingsTable: "mock.shop_shopify_listings", + syncGroupListingsTable: "mock.shop_shopify_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_shopify_sync_group_listing_drafts", }, { platform: SquareOnline, shopTable: "mock.shop_square_online", listingsTable: "mock.shop_square_online_listings", + syncGroupListingsTable: "mock.shop_square_online_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_square_online_sync_group_listing_drafts", }, { platform: Squarespace, shopTable: "mock.shop_squarespace", listingsTable: "mock.shop_squarespace_listings", + syncGroupListingsTable: "mock.shop_squarespace_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_squarespace_sync_group_listing_drafts", }, { platform: Tiktok, shopTable: "mock.shop_tiktok", listingsTable: "mock.shop_tiktok_listings", + syncGroupListingsTable: "mock.shop_tiktok_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_tiktok_sync_group_listing_drafts", }, { platform: WalmartMarketplace, shopTable: "mock.shop_walmart_marketplace", listingsTable: "mock.shop_walmart_marketplace_listings", + syncGroupListingsTable: "mock.shop_walmart_marketplace_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_walmart_marketplace_sync_group_listing_drafts", }, { platform: Wix, shopTable: "mock.shop_wix", listingsTable: "mock.shop_wix_listings", + syncGroupListingsTable: "mock.shop_wix_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_wix_sync_group_listing_drafts", }, { platform: WooCommerce, shopTable: "mock.shop_woo_commerce", listingsTable: "mock.shop_woo_commerce_listings", + syncGroupListingsTable: "mock.shop_woo_commerce_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_woo_commerce_sync_group_listing_drafts", }, { platform: Zoho, shopTable: "mock.shop_zoho", listingsTable: "mock.shop_zoho_listings", + syncGroupListingsTable: "mock.shop_zoho_sync_group_listings", syncGroupListingDraftsTable: "mock.shop_zoho_sync_group_listing_drafts", }, } diff --git a/domains/accounts/store_with_context.go b/domains/accounts/store_with_context.go index de3626f..023825c 100644 --- a/domains/accounts/store_with_context.go +++ b/domains/accounts/store_with_context.go @@ -6,6 +6,7 @@ package accounts import ( "context" "github.com/google/uuid" + "github.com/jackc/pgx/v5" ) type StoreWithContext struct { @@ -56,6 +57,10 @@ func (v_ctx *StoreWithContext) GetMockListingsForShop(acctID int64, platform Pla return v_ctx.Store.GetMockListingsForShop(v_ctx.ctx, acctID, platform, shopID) } +func (v_ctx *StoreWithContext) GetMockListing(acctID int64, platform Platform, shopID string, listingID string) (*Listing, error) { + return v_ctx.Store.GetMockListing(v_ctx.ctx, acctID, platform, shopID, listingID) +} + func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) { return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID) } @@ -151,3 +156,15 @@ func (v_ctx *StoreWithContext) GetMockSyncGroupListingDrafts(acctID int64) ([]Sy func (v_ctx *StoreWithContext) SaveNewSyncGroup(acctID int64) (SyncGroup, error) { return v_ctx.Store.SaveNewSyncGroup(v_ctx.ctx, acctID) } + +func (v_ctx *StoreWithContext) SaveNewMockSyncGroup(acctID int64, name string) (SyncGroup, error) { + return v_ctx.Store.SaveNewMockSyncGroup(v_ctx.ctx, acctID, name) +} + +func (v_ctx *StoreWithContext) ListMockSyncGroups(acctID int64) ([]SyncGroup, error) { + return v_ctx.Store.ListMockSyncGroups(v_ctx.ctx, acctID) +} + +func (v_ctx *StoreWithContext) listMockSyncGroupListings(tx pgx.Tx, acctID int64, syncGroupID int64) ([]SyncGroupListing, error) { + return v_ctx.Store.listMockSyncGroupListings(v_ctx.ctx, tx, acctID, syncGroupID) +} diff --git a/domains/accounts/sync_groups.go b/domains/accounts/sync_groups.go index f70f2cc..929e8b0 100644 --- a/domains/accounts/sync_groups.go +++ b/domains/accounts/sync_groups.go @@ -15,6 +15,7 @@ import ( type ( SyncGroup struct { SyncGroupIDs + Name string Listings []SyncGroupListing } @@ -811,6 +812,372 @@ func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup, }, nil } +func (db *Store) SaveNewMockSyncGroup(ctx context.Context, acctID int64, name string) (SyncGroup, error) { + tx, err := db.db.Begin(ctx) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to start transaction: %w", err) + } + defer tx.Rollback(ctx) + + // verify there are enough listings + + rows, err := tx.Query( + ctx, + ` + SELECT + COUNT(*) + FROM + mock.sync_group_listing_draft_order_indexes + WHERE + account_id = @account_id + `, + pgx.NamedArgs{ + "account_id": acctID, + }, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query: %w", err) + } + + numDrafts, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to scan rows: %w", err) + } + + if numDrafts < 2 { + return SyncGroup{}, fmt.Errorf("%w: insufficient listings: must be at least 2: %d", consts.ErrConflict, numDrafts) + } + + // create the new sync group row + + rows, err = tx.Query( + ctx, + ` + INSERT INTO + mock.sync_groups ( + account_id, + name + ) + VALUES ( + @account_id, + @name + ) + RETURNING + sync_group_id + `, + pgx.NamedArgs{ + "account_id": acctID, + "name": name, + }, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query to insert new mock sync group: %w", err) + } + + syncGroupID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64]) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to scan row from inserting new mock sync group: %w", err) + } + + // read and delete the draft listings + + type DraftListingRow struct { + Order_index int + Shop_id pgtype.Text + Listing_id pgtype.Text + } + infos := getAllMockShopSchemaInfos() + newListingsPerPlatform := make(map[Platform][]SyncGroupListing, len(infos)) + var numListings int + for _, info := range infos { + rows, err := tx.Query( + ctx, + fmt.Sprintf( + ` + DELETE FROM + %s + WHERE + account_id = @account_id + RETURNING + order_index, + shop_id, + listing_id + `, + info.syncGroupListingDraftsTable, + ), + pgx.NamedArgs{ + "account_id": acctID, + }, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query deleting mock sync group listing draft order indexes for platform %s: %w", info.platform, err) + } + + vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[DraftListingRow]) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to scan rows: %w", err) + } + + ls := make([]SyncGroupListing, len(vs)) + for i, v := range vs { + if !v.Shop_id.Valid { + return SyncGroup{}, fmt.Errorf("%w: shop_id not specified on listing of order index %d", consts.ErrConflict, v.Order_index) + } + if !v.Listing_id.Valid { + return SyncGroup{}, fmt.Errorf("%w: listing_id not specified on listing of order index %d", consts.ErrConflict, v.Order_index) + } + + ls[i] = SyncGroupListing{ + SyncGroupIDs: SyncGroupIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + SyncGroupID: syncGroupID, + }, + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + Platform: info.platform, + ShopID: v.Shop_id.String, + }, + ListingID: v.Listing_id.String, + OrderIndex: v.Order_index, + } + } + + newListingsPerPlatform[info.platform] = ls + numListings += len(ls) + } + + _, err = tx.Exec( + ctx, + ` + DELETE FROM + mock.sync_group_listing_draft_order_indexes + `, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query deleting mock sync group listing draft order indexes: %w", err) + } + + // create new sync listing rows + + listings := make([]SyncGroupListing, 0, numListings) + for _, info := range infos { + for _, l := range newListingsPerPlatform[info.platform] { + _, err := tx.Exec( + ctx, + ` + INSERT INTO + mock.sync_group_listing_order_indexes ( + sync_group_id, + order_index + ) + VALUES ( + @sync_group_id, + @order_index + ) + `, + pgx.NamedArgs{ + "sync_group_id": syncGroupID, + "order_index": l.OrderIndex, + }, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query to insert row into mock.sync_group_listing_order_indexes: %w", err) + } + + _, err = tx.Exec( + ctx, + fmt.Sprintf( + ` + INSERT INTO + %s ( + account_id, + sync_group_id, + shop_id, + listing_id, + order_index + ) + VALUES ( + @account_id, + @sync_group_id, + @shop_id, + @listing_id, + @order_index + ) + `, + info.syncGroupListingsTable, + ), + pgx.NamedArgs{ + "account_id": acctID, + "sync_group_id": syncGroupID, + "order_index": l.OrderIndex, + "shop_id": l.ShopID, + "listing_id": l.ListingID, + }, + ) + if err != nil { + return SyncGroup{}, fmt.Errorf("failed to perform query to insert row into %s: %w", info.syncGroupListingsTable, err) + } + + listings = append(listings, l) + } + } + + if err := tx.Commit(ctx); err != nil { + return SyncGroup{}, fmt.Errorf("failed to commit transaction: %w", err) + } + + // prepare return value + + slices.SortFunc(listings, func(a, b SyncGroupListing) int { + return a.OrderIndex - b.OrderIndex + }) + + return SyncGroup{ + SyncGroupIDs: SyncGroupIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + SyncGroupID: syncGroupID, + }, + Name: name, + Listings: listings, + }, nil +} + +func (db *Store) ListMockSyncGroups(ctx context.Context, acctID int64) ([]SyncGroup, error) { + tx, err := db.db.BeginTx(ctx, pgx.TxOptions{ + AccessMode: pgx.ReadOnly, + }) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback(ctx) + + rows, err := tx.Query( + ctx, + ` + SELECT + sync_group_id, + name + FROM + mock.sync_groups + WHERE + account_id = @account_id + `, + pgx.NamedArgs{ + "account_id": acctID, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to perform query: %w", err) + } + + vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { + Sync_group_id int64 + Name string + }]) + if err != nil { + return nil, fmt.Errorf("failed to scan rows: %w", err) + } + + grps := make([]SyncGroup, len(vs)) + for i, v := range vs { + listings, err := db.listMockSyncGroupListings(ctx, tx, acctID, v.Sync_group_id) + if err != nil { + return nil, fmt.Errorf("failed to load listings: %w", err) + } + + grps[i] = SyncGroup{ + SyncGroupIDs: SyncGroupIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + SyncGroupID: v.Sync_group_id, + }, + Name: v.Name, + Listings: listings, + } + } + + if err := tx.Commit(ctx); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return grps, nil +} + +// listMockSyncGroupListings will sort the returned listings by order_index. +func (db *Store) listMockSyncGroupListings(ctx context.Context, tx pgx.Tx, acctID, syncGroupID int64) ([]SyncGroupListing, error) { + var listings []SyncGroupListing + for _, info := range getAllMockShopSchemaInfos() { + rows, err := tx.Query( + ctx, + fmt.Sprintf( + ` + SELECT + account_id, + shop_id, + listing_id, + order_index + FROM + %s + WHERE + account_id = @account_id + AND sync_group_id = @sync_group_id + `, + info.syncGroupListingsTable, + ), + pgx.NamedArgs{ + "account_id": acctID, + "sync_group_id": syncGroupID, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to perform query on %s: %w", info.syncGroupListingsTable, err) + } + + vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { + Account_id int64 + Shop_id string + Listing_id string + Order_index int + }]) + if err != nil { + return nil, fmt.Errorf("failed to scan rows from %s: %w", info.syncGroupListingsTable, err) + } + + for _, v := range vs { + listings = append(listings, SyncGroupListing{ + SyncGroupIDs: SyncGroupIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + SyncGroupID: syncGroupID, + }, + AccountShopIDs: AccountShopIDs{ + AccountIDs: AccountIDs{ + AccountID: acctID, + }, + Platform: info.platform, + ShopID: v.Shop_id, + }, + ListingID: v.Listing_id, + OrderIndex: v.Order_index, + }) + } + } + + slices.SortFunc(listings, func(a, b SyncGroupListing) int { + return a.OrderIndex - b.OrderIndex + }) + + return listings, nil +} + func deref[T any](ptr *T) T { if ptr == nil { var zero T diff --git a/server/api/accounts/router.go b/server/api/accounts/router.go index 8ba0829..4475905 100644 --- a/server/api/accounts/router.go +++ b/server/api/accounts/router.go @@ -56,6 +56,9 @@ func Routes( draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft)) 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)) + mockDraftListings := mockSyncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/mock/draft/listings")) mockDraftListings.POST("", response.Handler(as.createMockSyncGroupListingDraft)) mockDraftListings.PUT("/:orderIndex/shop", response.Handler(as.setShopInMockSyncGroupListingDraft)) @@ -486,6 +489,51 @@ func (s *accountSubrouter) deleteMockSyncGroupListingDraft(c *gin.Context) (resp return response.StatusOK(), nil } +// POST /:acctID/inventory/sync-groups/mock +func (s *accountSubrouter) saveNewMockSyncGroup(c *gin.Context) (response.Response, error) { + r := c.Request + ctx := r.Context() + acctID := auth.GetIdentity(ctx).Account.AccountID + + var ( + name string + ) + + err := param.Form("name", param.Text(&name)). + Unmarshal(c) + if err != nil { + return nil, err + } + + if _, err := s.accts.SaveNewMockSyncGroup(ctx, acctID, name); err != nil { + return nil, response.Errorf("failed to save new mock sync group: %w", response.ErrorFromConstant(err)) + } + + return response.StatusCreated(), nil +} + +// DELETE /:acctID/inventory/sync-groups/mock/:syncGroupID +func (s *accountSubrouter) deleteMockSyncGroup(c *gin.Context) (response.Response, error) { + r := c.Request + ctx := r.Context() + acctID := auth.GetIdentity(ctx).Account.AccountID + + var ( + syncGroupID int64 + ) + + if err := param.Path("syncGroupID", param.Int64(&syncGroupID)). + Unmarshal(c); err != nil { + return nil, err + } + + if err := s.accts.DeleteMockSyncGroup(ctx, acctID, syncGroupID); err != nil { + return nil, response.Errorf("failed to save new mock sync group: %w", response.ErrorFromConstant(err)) + } + + return response.StatusCreated(), nil +} + func lowerSnakeCase(s accounts.Platform) string { return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_")) } diff --git a/server/param/params.go b/server/param/params.go index 6e6eaf9..c832959 100644 --- a/server/param/params.go +++ b/server/param/params.go @@ -17,6 +17,10 @@ func Int(dst *int) encoding.TextUnmarshaler { return (*intText)(dst) } +func Int64(dst *int64) encoding.TextUnmarshaler { + return (*int64Text)(dst) +} + func Platform(dst *accounts.Platform) encoding.TextUnmarshaler { return (*platformText)(dst) } @@ -24,6 +28,7 @@ func Platform(dst *accounts.Platform) encoding.TextUnmarshaler { type ( rawText string intText int + int64Text int64 platformText accounts.Platform ) @@ -41,6 +46,15 @@ func (n *intText) UnmarshalText(text []byte) error { return nil } +func (n *int64Text) UnmarshalText(text []byte) error { + i, err := strconv.ParseInt(string(text), 10, 64) + if err != nil { + return err + } + *n = int64Text(i) + return nil +} + func (p *platformText) UnmarshalText(text []byte) error { v, err := accounts.NewPlatform(string(text)) if err != nil { diff --git a/styles/index.css b/styles/index.css index d47d2b9..aee26cf 100644 --- a/styles/index.css +++ b/styles/index.css @@ -232,9 +232,15 @@ .m-\[2em\] { margin: 2em; } + .mx-\[1em\] { + margin-inline: 1em; + } .my-\[0\.5em\] { margin-block: 0.5em; } + .my-\[1em\] { + margin-block: 1em; + } .my-\[1rem\] { margin-block: 1rem; } @@ -540,6 +546,11 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .group-open\:mb-\[1em\] { + &:is(:where(.group):is([open], :popover-open, :open) *) { + margin-bottom: 1em; + } + } .group-open\:border-b-\[0\.25rem\] { &:is(:where(.group):is([open], :popover-open, :open) *) { border-bottom-style: var(--tw-border-style); @@ -561,11 +572,26 @@ } } } + .details-content\:flex { + &::details-content { + display: flex; + } + } .details-content\:transform-\[translate\(0px\,-2em\)\] { &::details-content { transform: translate(0px,-2em); } } + .details-content\:flex-col { + &::details-content { + flex-direction: column; + } + } + .details-content\:items-center { + &::details-content { + align-items: center; + } + } .details-content\:border-transparent { &::details-content { border-color: transparent; diff --git a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/listings/{orderIndex.int}.html.tmpl b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/listings/{orderIndex.int}.html.tmpl index e76454e..ed05b3b 100644 --- a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/listings/{orderIndex.int}.html.tmpl +++ b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/listings/{orderIndex.int}.html.tmpl @@ -70,12 +70,16 @@ value="{{$shop.Platform}}-{{$shop.ShopID}}" {{- if $isSelectedShop }}selected{{- end }} > - {{ $shop.Name }} - {{$shop.Platform}} - {{$shop.ShopID}} - {{(eq $selectedShopPlatform $shop.Platform)}} - {{(eq $selectedShopID $shop.ShopID)}} + {{ $shop.Name }} {{- end }} + + {{- if $entry.Platform }}{{ $entry.Platform }}{{ else }}-{{ end }} + + {{- $selectedListing := "" }} {{- if $selectedShopID }} diff --git a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/index.html.tmpl b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/index.html.tmpl index 089c682..cfcf3cd 100644 --- a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/index.html.tmpl +++ b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/index.html.tmpl @@ -17,6 +17,9 @@ Shop + + Platform + Listing @@ -52,7 +55,7 @@ on rowUpdated checkIfAllRowsAreFilledOut() - send setDisabled(disabled: not allRowsFilledOut or numRows < 2) to #create-sync-group-button + send setDisabled(disabled: not allRowsFilledOut or numRows < 2) to #create-mock-sync-group-button " > {{- $listings := .Accounts.GetMockSyncGroupListingDrafts $acctID }} @@ -67,6 +70,9 @@ Shop + + Platform + Listing @@ -98,7 +104,7 @@ hidden hx-get="{{ printf "/ui/accounts/%d/inventory/sync-groups/mock/draft/table/save-sync-group-button" $acctID }}" hx-trigger="{{ (printf "sse:accounts_%d_inventory_sync-groups_mock_draft_listings" $acctID) }}" - hx-target="#create-sync-group-button" + hx-target="#create-mock-sync-group-button" hx-swap="outerHTML" > diff --git a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/save-sync-group-button.html.tmpl b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/save-sync-group-button.html.tmpl index 6b41413..181b23d 100644 --- a/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/save-sync-group-button.html.tmpl +++ b/templates/components/accounts/{acctID.int64}/inventory/sync-groups/mock/draft/table/save-sync-group-button.html.tmpl @@ -2,7 +2,7 @@ {{- $acctID := .PathParams.acctID }} -{{- $listings := .Accounts.GetSyncGroupListingDrafts $acctID }} +{{- $listings := .Accounts.GetMockSyncGroupListingDrafts $acctID }} {{- $numCompletedListings := 0 }} {{- range $l := $listings }} @@ -17,8 +17,7 @@ {{ component "button" - "ID" "create-sync-group-button" - "HXPost" (printf "/api/accounts/%d/sync-groups/mock" $acctID) + "ID" "create-mock-sync-group-button" "Class" "mt-[1em] mb-[1em]" "Text" "Save Sync Group" "Disabled" (or (not $enoughListings) (not $allCompleted)) diff --git a/templates/components/button.html.tmpl b/templates/components/button.html.tmpl index 3c86736..18600ed 100644 --- a/templates/components/button.html.tmpl +++ b/templates/components/button.html.tmpl @@ -39,7 +39,7 @@ hx-swap="{{.HXSwap}}" {{- end }} {{- if .HXVals }} - hx-vals="{{.HXVals}}" + hx-vals='{{.HXVals}}' {{- end }} {{- if .Disabled}} disabled diff --git a/templates/pages/accounts/{acctID.int64}/inventory/mock.html.tmpl b/templates/pages/accounts/{acctID.int64}/inventory/mock.html.tmpl index 3cc972e..789a597 100644 --- a/templates/pages/accounts/{acctID.int64}/inventory/mock.html.tmpl +++ b/templates/pages/accounts/{acctID.int64}/inventory/mock.html.tmpl @@ -7,7 +7,11 @@ {{- define "title" }} Manage Mock Inventory ⋅ Inventory++ {{ end }} -
+
- {{ component (printf "accounts/%d/inventory/sync-groups/mock/draft/table/add-listing-button" $acctID) }} - {{ component (printf "accounts/%d/inventory/sync-groups/mock/draft/table/save-sync-group-button" $acctID) }} - +
+ + +
+ {{ component (printf "accounts/%d/inventory/sync-groups/mock/draft/table/add-listing-button" $acctID) }} + {{ component (printf "accounts/%d/inventory/sync-groups/mock/draft/table/save-sync-group-button" $acctID) }} +
+
+ + +{{- $dot := . }} +
    + {{- range $grp := .Accounts.ListMockSyncGroups $acctID }} +
  • +
    + +

    + Name: {{ $grp.Name }} +

    +
    + + + + + + + + + + + + {{- range $listing := $grp.Listings }} + + {{- $val := $dot.Accounts.GetMockListing $acctID $listing.Platform $listing.ShopID $listing.ListingID }} + + + + + + {{- end }} + +
    + Name + + SKU + + Description + + Count +
    + {{ $val.Name }} + + {{ $val.SKU }} + + {{ $val.Description }} + + {{ $val.Count }} +
    + + {{- component "button" + "Class" "mt-[1em]" + "Text" "Delete Group" + "HXDelete" (printf "/api/accounts/%d/inventory/sync-groups/mock/%d" $acctID $grp.SyncGroupID) + "HXSwap" "none" + }} +
    +
  • + {{- end }} +