mock mode: database queries implemented and checkbox on account page built

This commit is contained in:
2026-02-26 22:54:58 -07:00
parent 85817a4adb
commit c6b8a9a00d
10 changed files with 278 additions and 27 deletions
@@ -0,0 +1,7 @@
BEGIN;
DROP TABLE mock_mode;
COMMIT;
@@ -0,0 +1,10 @@
BEGIN;
CREATE TABLE mock_mode (
account_id INTEGER PRIMARY KEY REFERENCES accounts,
mock_mode BOOLEAN NOT NULL DEFAULT FALSE
);
COMMIT;
+66
View File
@@ -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 {
+28
View File
@@ -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)
}
+28
View File
@@ -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
+14
View File
@@ -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 {
+14 -2
View File
@@ -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 {
+83 -25
View File
@@ -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;
}
@@ -0,0 +1,26 @@
{{- $acctID := .Identity.Account.AccountID }}
<section
class="m-[1rem]"
hx-get="/ui/accounts/{{$acctID}}/mock-mode-section"
hx-trigger="sse:accounts_{{$acctID}}_mock-mode"
hx-swap="morph"
>
{{- $mockMode := .Accounts.GetMockMode $acctID }}
<form id="mock-mode-form">
<label class="font-display text-[1.5em]">
Mock Mode
<input
type="checkbox"
{{- if $mockMode }}
checked
{{- end }}
class="h-[1em] w-[1em]"
hx-put="/api/accounts/{{$acctID}}/mock-mode"
hx-vals='{"on": {{not $mockMode}}}'
hx-swap="none"
/>
</label>
</form>
</section>
@@ -47,3 +47,5 @@
</div>
</h2>
</section>
{{ component (printf "accounts/%d/mock-mode-section" $acctID) }}