Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d5e7a70
test(desk): guard against browser dialogs and native selects in web apps
futurebuildai Oct 14, 2026
a0d0fc6
feat(desk): the app's own dialog with a promise API
futurebuildai Oct 14, 2026
d5314e6
feat(desk): the app's own select element
futurebuildai Oct 14, 2026
d80cf5b
refactor(desk): sweep the browser's dialogs to the app's own
futurebuildai Oct 14, 2026
70d79e7
refactor(desk): the modals' native selects become gable-select
futurebuildai Oct 14, 2026
da72ed3
refactor(desk): the back-office pages' native selects become gable-se…
futurebuildai Oct 14, 2026
1dd19a6
refactor(desk): the field, portal, counter and report pages' native s…
futurebuildai Oct 14, 2026
5b25af1
test(desk): drive the app's dialog and select in the page legs
futurebuildai Oct 14, 2026
1bd023a
chore(desk): drop imports the select sweep left unused
futurebuildai Oct 14, 2026
db515a2
fix(desk): type the gable-select list rows for the build's tsc -b
futurebuildai Oct 14, 2026
148df5c
test(e2e): drive the app's dialog and select from the specs
futurebuildai Oct 14, 2026
ce1e7a2
fix(desk): a pick inside a label-wrapped gable-select stays picked
futurebuildai Oct 14, 2026
45fea7a
Merge remote-tracking branch 'origin/refactor/v1' into refactor/g1-ap…
futurebuildai Oct 14, 2026
98ee7e3
test(desk): the merged-in Clear sale tender legs answer the app's dialog
futurebuildai Oct 14, 2026
b261814
test(desk): red legs for the app dialog in the browser's top layer
futurebuildai Oct 14, 2026
1f007bd
fix(desk): the app's dialog renders in the browser's top layer
futurebuildai Oct 14, 2026
7151104
test(desk): red legs for focus when a select pick opens a dialog
futurebuildai Oct 14, 2026
cf9265b
fix(desk): a select pick hands the focus over cleanly
futurebuildai Oct 14, 2026
c471711
test(desk): red leg for the select speaking as a combobox
futurebuildai Oct 14, 2026
eba66cd
fix(desk): the select is a combobox that announces the active option
futurebuildai Oct 14, 2026
5c124a8
test(desk): red leg for a select named by its field label
futurebuildai Oct 14, 2026
d830240
fix(desk): a select named by label[for] takes its field name back
futurebuildai Oct 14, 2026
2e9e761
test(desk): red legs for the list living in the top layer
futurebuildai Oct 14, 2026
5b427f1
fix(desk): the select's list opens in the browser's top layer
futurebuildai Oct 14, 2026
06d6341
test(desk): red legs for required selects that say what is missing
futurebuildai Oct 14, 2026
bbc6f53
fix(desk): the four required selects say what is missing
futurebuildai Oct 14, 2026
fa26940
fix(desk): drop the stale chevron overlay beside the counter's job pi…
futurebuildai Oct 14, 2026
6a74bc3
test(desk): red legs for the flipped list hugging its button
futurebuildai Oct 14, 2026
e15af13
fix(desk): a flipped list clears the edge it does not use
futurebuildai Oct 14, 2026
b2a563e
test(desk): red leg for a panel-edge click keeping a typed prompt
futurebuildai Oct 14, 2026
4db02e2
fix(desk): the dialog's padding is the panel's, so only the backdrop …
futurebuildai Oct 14, 2026
9128222
Merge remote-tracking branch 'origin/refactor/v1' into refactor/g1-ap…
futurebuildai Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions web/apps/desk/src/components/crm/LogActivityModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

import { LitElement, html } from 'lit';
import '../ui/gable-select';
import { customElement, property, state } from 'lit/decorators.js';
import type { CreateActivityRequest, Contact, ActivityType } from '../../types/crm';
import { crmApi } from '../../services/crmApi';
Expand Down Expand Up @@ -89,17 +90,12 @@ export class GableLogActivityModal extends LitElement {

<div class="mb-4">
<label for="log-activity-contact" class="block text-sm font-medium text-zinc-300 mb-1">Contact (Optional)</label>
<select
<gable-select
id="log-activity-contact"
.value=${this._contactId}
.options=${[{ value: '', label: '-- No Specific Contact --' }, ...this.contacts.map(c => ({ value: c.id, label: `${c.first_name} ${c.last_name} (${c.role})` }))]}
@change=${(e: Event) => this._contactId = (e.target as HTMLSelectElement).value}
class="bg-zinc-800 text-white w-full px-3 py-2 border border-white/20 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value="">-- No Specific Contact --</option>
${this.contacts.map(c => html`
<option value=${c.id}>${c.first_name} ${c.last_name} (${c.role})</option>
`)}
</select>
></gable-select>
</div>

<div class="mb-6">
Expand Down
19 changes: 7 additions & 12 deletions web/apps/desk/src/components/inventory/AddProductModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

import { LitElement, html, nothing } from 'lit';
import '../ui/gable-select';
import { customElement, property, state } from 'lit/decorators.js';
import { UOM_OPTIONS } from '../../types/product';
import type { ProductCreate, UOM } from '../../types/product';
Expand Down Expand Up @@ -179,13 +180,11 @@ export class GableAddProductModal extends LitElement {

<div>
<label class="block text-sm font-medium text-zinc-400 mb-1">Primary UOM</label>
<select
<gable-select
.value=${this._uom}
.options=${UOM_OPTIONS.map((opt) => ({ value: opt, label: opt }))}
@change=${(e: Event) => this._uom = (e.target as HTMLSelectElement).value as UOM}
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-zinc-100 focus:outline-none focus:ring-2 focus:ring-amber-600 focus:border-transparent"
>
${UOM_OPTIONS.map((opt) => html`<option value=${opt}>${opt}</option>`)}
</select>
></gable-select>
</div>

<div class="grid grid-cols-2 gap-4">
Expand All @@ -201,15 +200,11 @@ export class GableAddProductModal extends LitElement {
</div>
<div>
<label class="block text-sm font-medium text-zinc-400 mb-1">Vendor / Manufacturer</label>
<select
<gable-select
.value=${this._vendorId}
.options=${[{ value: '', label: '— Select vendor —' }, ...this._vendors.map((v) => ({ value: v.id, label: v.name })), { value: NEW_VENDOR_SENTINEL, label: '+ New vendor…' }]}
@change=${(e: Event) => this._vendorId = (e.target as HTMLSelectElement).value}
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-zinc-100 focus:outline-none focus:ring-2 focus:ring-amber-600 focus:border-transparent"
>
<option value="">— Select vendor —</option>
${this._vendors.map((v) => html`<option value=${v.id}>${v.name}</option>`)}
<option value=${NEW_VENDOR_SENTINEL}>+ New vendor…</option>
</select>
></gable-select>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: LicenseRef-OpenLBM-Surface-1.0
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

/**
* Transfer Stock asks for a source and a destination with the app's own
* selects. Confirming without a destination must say what is missing in
* words, not fail silently (the browser's own refusal went away with the
* native selects).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import './InventoryTransferModal'
import type { GableInventoryTransferModal } from './InventoryTransferModal'
import type { Product } from '../../types/product'
import { mountAsync, update, q, text, jsonResponse } from '../../test/dom.ts'

const PRODUCT = { id: 'p-1', sku: 'SKU-1', description: 'Stud' } as unknown as Product
const LEVELS = { items: [{ location_id: 'loc-a', location_name: 'A bin', quantity: '50000' }], next_cursor: null }
const LOCATIONS = {
items: [
{ id: 'loc-a', code: 'A', path: 'A', type: 'bin' },
{ id: 'loc-b', code: 'B', path: 'B', type: 'bin' },
],
next_cursor: null,
}

let posts: string[]

beforeEach(() => {
localStorage.setItem('token', 'test-token')
posts = []
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
if (init?.method === 'POST') {
posts.push(url)
return jsonResponse({})
}
if (url.includes('/api/v1/inventory/levels')) return jsonResponse(LEVELS)
if (url.includes('/api/v1/locations')) return jsonResponse(LOCATIONS)
throw new Error(`unexpected fetch: ${init?.method ?? 'GET'} ${url}`)
}))
})

afterEach(() => {
vi.unstubAllGlobals()
vi.restoreAllMocks()
})

async function open(): Promise<GableInventoryTransferModal> {
return mountAsync<GableInventoryTransferModal>('gable-inventory-transfer-modal', { isOpen: true, product: PRODUCT })
}

describe('gable-inventory-transfer-modal required picks', () => {
it('the source and destination selects read as required', async () => {
const el = await open()
const selects = Array.from(el.querySelectorAll('gable-select'))
expect(selects.length).toBe(2)
for (const s of selects) {
expect(s.querySelector('button')!.getAttribute('aria-required')).toBe('true')
}
})

it('a confirm with no destination says what is missing and sends nothing', async () => {
const el = await open()
const qty = q<HTMLInputElement>(el, 'input[type="number"]')
qty.value = '1'
qty.dispatchEvent(new Event('input', { bubbles: true }))
await update(el, {})
q<HTMLFormElement>(el, 'form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
await update(el, {})
const message = q<HTMLElement>(el, '[data-testid="transfer-form-error"]')
expect(message.getAttribute('role')).toBe('alert')
expect(text(message)).toContain('destination')
expect(posts).toEqual([])
})
})
49 changes: 24 additions & 25 deletions web/apps/desk/src/components/inventory/InventoryTransferModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

import { LitElement, html, nothing } from 'lit';
import '../ui/gable-select';
import { customElement, property, state } from 'lit/decorators.js';
import { icon } from '../../lib/icons';
import { X } from 'lucide';
Expand All @@ -24,6 +25,7 @@ export class GableInventoryTransferModal extends LitElement {
@state() private _quantity = '';
@state() private _reason = '';
@state() private _isSubmitting = false;
@state() private _formError = '';
@state() private _inventory: Inventory[] = [];
@state() private _locations: Location[] = [];

Expand All @@ -33,6 +35,7 @@ export class GableInventoryTransferModal extends LitElement {
this._quantity = '';
this._reason = '';
this._toLoc = '';
this._formError = '';
}
}

Expand Down Expand Up @@ -60,7 +63,12 @@ export class GableInventoryTransferModal extends LitElement {

private async _handleSubmit(e: Event) {
e.preventDefault();
if (!this.product || !this._fromLoc || !this._toLoc || !this._quantity) return;
if (!this.product) return;
if (!this._fromLoc || !this._toLoc || !this._quantity) {
this._formError = 'Choose a source and a destination, and enter a quantity.';
return;
}
this._formError = '';

this._isSubmitting = true;
try {
Expand Down Expand Up @@ -111,36 +119,24 @@ export class GableInventoryTransferModal extends LitElement {
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm text-gray-400 mb-2">From Location</label>
<select
.value=${this._fromLoc}
@change=${(e: Event) => this._fromLoc = (e.target as HTMLSelectElement).value}
class="w-full bg-deep-space border border-white/20 rounded p-3 text-white"
<gable-select
required
>
<option value="">Select Source...</option>
${this._inventory.filter(i => i.location_id).map(i => html`
<option value=${i.location_id}>
${i.location_name} (${formatQuantity(i.quantity)})
</option>
`)}
</select>
?invalid=${this._formError !== '' && !this._fromLoc}
.value=${this._fromLoc}
.options=${[{ value: '', label: 'Select Source...' }, ...this._inventory.filter(i => i.location_id).map(i => ({ value: String(i.location_id), label: `${i.location_name} (${formatQuantity(i.quantity)})` }))]}
@change=${(e: Event) => { this._fromLoc = (e.target as HTMLSelectElement).value; this._formError = ''; }}
></gable-select>
</div>

<div>
<label class="block text-sm text-gray-400 mb-2">To Location</label>
<select
.value=${this._toLoc}
@change=${(e: Event) => this._toLoc = (e.target as HTMLSelectElement).value}
class="w-full bg-deep-space border border-white/20 rounded p-3 text-white"
<gable-select
required
>
<option value="">Select Dest...</option>
${this._locations
.filter(l => l.id !== this._fromLoc)
.map(l => html`
<option value=${l.id}>${l.path || l.code}</option>
`)}
</select>
?invalid=${this._formError !== '' && !this._toLoc}
.value=${this._toLoc}
.options=${[{ value: '', label: 'Select Dest...' }, ...this._locations.filter(l => l.id !== this._fromLoc).map(l => ({ value: l.id, label: l.path || l.code }))]}
@change=${(e: Event) => { this._toLoc = (e.target as HTMLSelectElement).value; this._formError = ''; }}
></gable-select>
</div>
</div>

Expand Down Expand Up @@ -169,6 +165,9 @@ export class GableInventoryTransferModal extends LitElement {
/>
</div>

${this._formError ? html`
<p class="text-sm text-safety-red" role="alert" data-testid="transfer-form-error">${this._formError}</p>
` : nothing}
<div class="flex gap-4 pt-4">
<button
type="button"
Expand Down
25 changes: 7 additions & 18 deletions web/apps/desk/src/components/inventory/StockAdjustmentModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

import { LitElement, html, nothing } from 'lit';
import '../ui/gable-select';
import { customElement, property, state } from 'lit/decorators.js';
import type { Product } from '../../types/product';
import type { Location } from '../../types/location';
Expand Down Expand Up @@ -80,17 +81,11 @@ export class GableStockAdjustmentModal extends LitElement {
<form @submit=${this._handleSubmit} class="space-y-4">
<div>
<label class="block text-xs uppercase tracking-wider text-gray-500 mb-1">Location</label>
<select
class="w-full bg-black/20 border border-white/10 rounded px-3 py-2 text-white focus:border-gable-green outline-none"
<gable-select
.value=${this._selectedLocationId}
.options=${this._locations.map(loc => ({ value: loc.id, label: `${loc.path || loc.code} (${loc.type.toUpperCase()})` }))}
@change=${(e: Event) => this._selectedLocationId = (e.target as HTMLSelectElement).value}
>
${this._locations.map(loc => html`
<option value=${loc.id}>
${loc.path || loc.code} (${loc.type.toUpperCase()})
</option>
`)}
</select>
></gable-select>
</div>

<div>
Expand All @@ -107,17 +102,11 @@ export class GableStockAdjustmentModal extends LitElement {

<div>
<label class="block text-xs uppercase tracking-wider text-gray-500 mb-1">Reason Code</label>
<select
class="w-full bg-black/20 border border-white/10 rounded px-3 py-2 text-white focus:border-gable-green outline-none"
<gable-select
.value=${this._reason}
.options=${['Receipt', 'Cycle Count', 'Damaged', 'Return', 'Found'].map(r => ({ value: r, label: r }))}
@change=${(e: Event) => this._reason = (e.target as HTMLSelectElement).value}
>
<option>Receipt</option>
<option>Cycle Count</option>
<option>Damaged</option>
<option>Return</option>
<option>Found</option>
</select>
></gable-select>
</div>

<div class="flex justify-end gap-3 mt-8">
Expand Down
7 changes: 3 additions & 4 deletions web/apps/desk/src/components/invoices/PaymentModal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { describe, it, expect, vi } from 'vitest'
import './PaymentModal'
import type { GablePaymentModal } from './PaymentModal'
import { mount, update, q } from '../../test/dom'
import { mount, update, q, chooseOption } from '../../test/dom'
import type { CreatePaymentRequest } from '../../types/payment'

function open(amountDueCents: number) {
Expand Down Expand Up @@ -113,9 +113,8 @@ describe('gable-payment-modal — cents out', () => {

it('carries the method and reference through, lowercase as the route takes them', async () => {
const el = await open(2500)
const select = q<HTMLSelectElement>(el, 'select')
select.value = 'check'
select.dispatchEvent(new Event('change', { bubbles: true }))
const select = q<HTMLElement & { value: string }>(el, 'gable-select')
await chooseOption(select, 'check')
await update(el, {})

const payload = submit(el)
Expand Down
9 changes: 4 additions & 5 deletions web/apps/desk/src/components/invoices/PaymentModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors

import { LitElement, html, nothing } from 'lit';
import '../ui/gable-select';
import { customElement, property, state } from 'lit/decorators.js';
import type { CreatePaymentRequest, PaymentMethod } from '../../types/payment';

Expand Down Expand Up @@ -97,13 +98,11 @@ export class GablePaymentModal extends LitElement {

<div>
<label class="block text-sm font-medium text-zinc-400 mb-1">Method</label>
<select
<gable-select
.value=${this._method}
.options=${PAYMENT_METHODS.map(m => ({ value: m, label: METHOD_LABELS[m] }))}
@change=${(e: Event) => this._method = (e.target as HTMLSelectElement).value as PaymentMethod}
class="w-full bg-zinc-950 border border-zinc-700 rounded px-3 py-2 text-zinc-100 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
${PAYMENT_METHODS.map(m => html`<option value=${m}>${METHOD_LABELS[m]}</option>`)}
</select>
></gable-select>
</div>

<div>
Expand Down
Loading
Loading