diff --git a/database_migrations/000025_mock_mode.down.sql b/database_migrations/000025_mock_mode.down.sql new file mode 100644 index 0000000..3f99466 --- /dev/null +++ b/database_migrations/000025_mock_mode.down.sql @@ -0,0 +1,7 @@ +BEGIN; + + +DROP TABLE mock_mode; + + +COMMIT; diff --git a/database_migrations/000025_mock_mode.up.sql b/database_migrations/000025_mock_mode.up.sql new file mode 100644 index 0000000..8a279e6 --- /dev/null +++ b/database_migrations/000025_mock_mode.up.sql @@ -0,0 +1,10 @@ +BEGIN; + + +CREATE TABLE mock_mode ( + account_id INTEGER PRIMARY KEY REFERENCES accounts, + mock_mode BOOLEAN NOT NULL DEFAULT FALSE +); + + +COMMIT; diff --git a/domains/accounts/mocks.go b/domains/accounts/mocks.go index ccd39bf..50ac576 100644 --- a/domains/accounts/mocks.go +++ b/domains/accounts/mocks.go @@ -27,6 +27,72 @@ type ( } ) +func (db *Store) SetMockMode(ctx context.Context, acctID int64, on bool) error { + _, err := db.db.Exec( + ctx, + ` + INSERT INTO + mock_mode ( + account_id, + mock_mode + ) + VALUES ( + @account_id, + @mock_mode + ) + ON CONFLICT (account_id) DO UPDATE + SET + mock_mode = @mock_mode + `, + pgx.NamedArgs{ + "account_id": acctID, + "mock_mode": on, + }, + ) + if err != nil { + return fmt.Errorf("failed to perform query: %w", err) + } + + return nil +} + +func (db *Store) GetMockMode(ctx context.Context, acctID int64) (on bool, err error) { + rows, err := db.db.Query( + ctx, + ` + WITH account_query AS ( + SELECT + account_id + FROM + accounts + WHERE + account_id = @account_id + ) + SELECT + COALESCE(mm.mock_mode, false) + FROM + account_query AS aq + LEFT JOIN + mock_mode AS mm + USING + (account_id) + `, + pgx.NamedArgs{ + "account_id": acctID, + }, + ) + if err != nil { + return false, fmt.Errorf("failed to perform query: %w", err) + } + + on, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]) + if err != nil { + return false, fmt.Errorf("failed to scan rows: %w", err) + } + + return on, nil +} + func (db *Store) CreateMockShop(ctx context.Context, acctID int64, platform Platform, name string) (shopID uuid.UUID, err error) { shopTableName, err := getMockShopTableName(platform) if err != nil { diff --git a/domains/accounts/store_with_context.go b/domains/accounts/store_with_context.go index fdb42d4..2c4fe0f 100644 --- a/domains/accounts/store_with_context.go +++ b/domains/accounts/store_with_context.go @@ -69,6 +69,10 @@ func (v_ctx *StoreWithContext) StartEditingMockSyncGroup(acctID int64, syncGroup return v_ctx.Store.StartEditingMockSyncGroup(v_ctx.ctx, acctID, syncGroupID) } +func (v_ctx *StoreWithContext) SaveMockSyncGroupBeingEdited(acctID int64) (int64, error) { + return v_ctx.Store.SaveMockSyncGroupBeingEdited(v_ctx.ctx, acctID) +} + func (v_ctx *StoreWithContext) CancelEditingOfMockSyncGroupForAccount(acctID int64) (int64, bool, error) { return v_ctx.Store.CancelEditingOfMockSyncGroupForAccount(v_ctx.ctx, acctID) } @@ -85,10 +89,22 @@ func (v_ctx *StoreWithContext) GetMockSyncGroupEditingListing(acctID int64, sync return v_ctx.Store.GetMockSyncGroupEditingListing(v_ctx.ctx, acctID, syncGroupID, orderIndex) } +func (v_ctx *StoreWithContext) AddListingToMockSyncGroupBeingEdited(acctID int64) (int64, int, error) { + return v_ctx.Store.AddListingToMockSyncGroupBeingEdited(v_ctx.ctx, acctID) +} + 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) SetListingInListingInMockSyncGroupBeingEdited(acctID int64, syncGroupID int64, orderIndex int, listingID string) error { + return v_ctx.Store.SetListingInListingInMockSyncGroupBeingEdited(v_ctx.ctx, acctID, syncGroupID, orderIndex, listingID) +} + +func (v_ctx *StoreWithContext) DeleteListingInListingInMockSyncGroupBeingEdited(acctID int64, orderIndex int) error { + return v_ctx.Store.DeleteListingInListingInMockSyncGroupBeingEdited(v_ctx.ctx, acctID, orderIndex) +} + func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) { return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID) } @@ -97,6 +113,18 @@ func (v_ctx *StoreWithContext) beginReadonlyTxn(cb func(pgx.Tx) error) error { return v_ctx.Store.beginReadonlyTxn(v_ctx.ctx, cb) } +func (v_ctx *StoreWithContext) beginTxn(cb func(pgx.Tx) error) error { + return v_ctx.Store.beginTxn(v_ctx.ctx, cb) +} + +func (v_ctx *StoreWithContext) SetMockMode(acctID int64, on bool) error { + return v_ctx.Store.SetMockMode(v_ctx.ctx, acctID, on) +} + +func (v_ctx *StoreWithContext) GetMockMode(acctID int64) (bool, error) { + return v_ctx.Store.GetMockMode(v_ctx.ctx, acctID) +} + 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) } diff --git a/server/api/accounts/router.go b/server/api/accounts/router.go index 9b55897..8598cdd 100644 --- a/server/api/accounts/router.go +++ b/server/api/accounts/router.go @@ -37,6 +37,8 @@ func Routes( r.POST("", response.Handler(as.createAccount)) + // TODO: make the subroutes more readable + platformGroup := r.Group("/:acctID/platforms/:platform", pub.Publish("/:acctID/platforms")) platformGroup.PUT("/order-index", response.Handler(as.setOrderOfPlatformOnAccountPage)) platformGroup.POST("/shops/mocks", response.Handler(as.createMockShop)) @@ -55,6 +57,9 @@ func Routes( draftListings.PUT("/:orderIndex/listing", response.Handler(as.setListingInSyncGroupListingDraft)) draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft)) + mockModeGroup := r.Group("/:acctID/mock-mode", pub.Publish("/:acctID/mock-mode")) + mockModeGroup.PUT("", response.Handler(as.setMockMode)) + 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)) @@ -500,6 +505,29 @@ func (s *accountSubrouter) deleteMockSyncGroupListingDraft(c *gin.Context) (resp return response.StatusOK(), nil } +// PUT /:acctID/mock-mode +func (s *accountSubrouter) setMockMode(c *gin.Context) (response.Response, error) { + r := c.Request + ctx := r.Context() + acctID := auth.GetIdentity(ctx).Account.AccountID + + var ( + on bool + ) + + err := param.Form("on", param.Bool(&on)). + Unmarshal(c) + if err != nil { + return nil, err + } + + if err := s.accts.SetMockMode(ctx, acctID, on); err != nil { + return nil, response.Errorf("failed to set mock mode: %w", response.ErrorFromConstant(err)) + } + + return response.StatusOK(), nil +} + // POST /:acctID/inventory/sync-groups/mock func (s *accountSubrouter) saveNewMockSyncGroup(c *gin.Context) (response.Response, error) { r := c.Request diff --git a/server/param/params.go b/server/param/params.go index c832959..9c2a25d 100644 --- a/server/param/params.go +++ b/server/param/params.go @@ -21,6 +21,10 @@ func Int64(dst *int64) encoding.TextUnmarshaler { return (*int64Text)(dst) } +func Bool(dst *bool) encoding.TextUnmarshaler { + return (*boolText)(dst) +} + func Platform(dst *accounts.Platform) encoding.TextUnmarshaler { return (*platformText)(dst) } @@ -29,6 +33,7 @@ type ( rawText string intText int int64Text int64 + boolText bool platformText accounts.Platform ) @@ -55,6 +60,15 @@ func (n *int64Text) UnmarshalText(text []byte) error { return nil } +func (b *boolText) UnmarshalText(text []byte) error { + v, err := strconv.ParseBool(string(text)) + if err != nil { + return err + } + *b = boolText(v) + return nil +} + func (p *platformText) UnmarshalText(text []byte) error { v, err := accounts.NewPlatform(string(text)) if err != nil { diff --git a/server/response/error.go b/server/response/error.go index 94e48e2..cca6d89 100644 --- a/server/response/error.go +++ b/server/response/error.go @@ -167,10 +167,22 @@ func ErrorFromConstant(err error) error { cerr := err for cerr != nil { switch cerr { + // if the error already is an ErrorResponse with the appropriate status, nothing needs to be done. + // else the error needs to be wrapped in the appropriate ErrorResponse. case consts.ErrNotFound: - return NotFound() + if er, ok := GetError(err); ok { + if s, ok := er.GetStatus(); ok && s == http.StatusNotFound { + return err + } + } + return NotFound().Wrap(err) case consts.ErrConflict: - return Conflict() + if er, ok := GetError(err); ok { + if s, ok := er.GetStatus(); ok && s == http.StatusConflict { + return err + } + } + return Conflict().Wrap(err) } uerr, ok := cerr.(interface { diff --git a/styles/index.css b/styles/index.css index 1319e16..9e18b2a 100644 --- a/styles/index.css +++ b/styles/index.css @@ -197,6 +197,29 @@ .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; @@ -322,6 +345,9 @@ .table { display: table; } + .h-\[1em\] { + height: 1em; + } .h-\[2rem\] { height: 2rem; } @@ -340,6 +366,9 @@ .min-h-full { min-height: 100%; } + .w-\[1em\] { + width: 1em; + } .w-fit { width: fit-content; } @@ -361,6 +390,9 @@ .flex-shrink { flex-shrink: 1; } + .flex-grow { + flex-grow: 1; + } .flex-grow-\[1\] { flex-grow: 1; } @@ -376,6 +408,12 @@ .basis-full { flex-basis: 100%; } + .border-collapse { + border-collapse: collapse; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } .animate-pulse { animation: var(--animate-pulse); } @@ -536,6 +574,9 @@ .font-display { font-family: var(--display-family); } + .font-text { + font-family: var(--text-family); + } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -548,6 +589,13 @@ font-size: var(--text-xl); line-height: var(--tw-leading, var(--text-xl--line-height)); } + .text-\[1\.5em\] { + font-size: 1.5em; + } + .font-\[1\.5em\] { + --tw-font-weight: 1.5em; + font-weight: 1.5em; + } .font-bold { --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); @@ -559,6 +607,12 @@ .text-nowrap { text-wrap: nowrap; } + .text-wrap { + text-wrap: wrap; + } + .text-inherit { + color: inherit; + } .capitalize { text-transform: capitalize; } @@ -568,6 +622,10 @@ .underline { text-decoration-line: underline; } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } .outline-1 { outline-style: var(--tw-outline-style); outline-width: 1px; @@ -1142,6 +1200,26 @@ } } } +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} @property --tw-border-style { syntax: "*"; inherits: false; @@ -1224,26 +1302,6 @@ inherits: false; initial-value: 1; } -@property --tw-rotate-x { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-y { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-z { - syntax: "*"; - inherits: false; -} -@property --tw-skew-x { - syntax: "*"; - inherits: false; -} -@property --tw-skew-y { - syntax: "*"; - inherits: false; -} @property --tw-content { syntax: "*"; initial-value: ""; @@ -1261,6 +1319,11 @@ @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; --tw-border-style: solid; --tw-font-weight: initial; --tw-outline-style: solid; @@ -1280,11 +1343,6 @@ --tw-scale-x: 1; --tw-scale-y: 1; --tw-scale-z: 1; - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; --tw-content: ""; --tw-leading: initial; } diff --git a/templates/components/accounts/{acctID.int64}/mock-mode-section.html.tmpl b/templates/components/accounts/{acctID.int64}/mock-mode-section.html.tmpl new file mode 100644 index 0000000..9ffbf70 --- /dev/null +++ b/templates/components/accounts/{acctID.int64}/mock-mode-section.html.tmpl @@ -0,0 +1,26 @@ +{{- $acctID := .Identity.Account.AccountID }} + +
+ {{- $mockMode := .Accounts.GetMockMode $acctID }} + +
+ +
+
diff --git a/templates/pages/accounts/{acctID.int64}/index.html.tmpl b/templates/pages/accounts/{acctID.int64}/index.html.tmpl index b2c79bc..bb2ed29 100644 --- a/templates/pages/accounts/{acctID.int64}/index.html.tmpl +++ b/templates/pages/accounts/{acctID.int64}/index.html.tmpl @@ -47,3 +47,5 @@ + +{{ component (printf "accounts/%d/mock-mode-section" $acctID) }}