Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions src/apps/sales/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Report metadata determines every displayed column, including grouped Stage.
Search and column substring filters apply on **Apply**; headers sort globally
before server pagination. Changing a filter, sort or page size starts at page
one. Clear resets filters and sorting. Null display values use an em dash.
Search sales, Filter field and Contains use persistent labels above equal-height
controls, aligned in the desktop filter row and stacked on mobile.
The semantic HTML table provides keyboard sorting, `aria-sort`, and a focusable
horizontal scroll region for wide reports; the shared Table component performs
client sorting and is intentionally not used for this server-paginated report.
Expand All @@ -25,8 +27,8 @@ are explicit. `allData: false` warns that filters and totals cover only received
rows, because Salesforce Analytics caps report details at 2,000 records.

The app opts into scoped shared 2026 foundations, using Figtree headings, Nunito
Sans body text, semantic color tokens, shared buttons, inputs and loading
controls. Source design guidance:
Sans body text, semantic color tokens, shared buttons and loading controls, and
explicitly labelled native filter controls. Source design guidance:
[Topcoder Design System — August 2026](https://www.figma.com/design/C2cA6508RhpjWJDp7MLKbO/Topcoder-Design-System---Aug-2026?node-id=1-54).

Run `nvm use` in platform-ui before `yarn lint`, `LOGICAL_ENV=dev yarn run build`,
Expand Down
11 changes: 7 additions & 4 deletions src/apps/sales/src/SalesPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@
.panelHeader h2 { font-size: $tc-2026-h4-size; line-height: 32px; }
.panelHeader p { margin-top: 4px; color: $tc-2026-muted; }
.filters { padding: 24px; display: flex; align-items: flex-end; flex-wrap: wrap; gap: 16px; background: $tc-2026-canvas; }
.search { flex: 1 1 220px; min-width: 160px; margin-bottom: 0; }
.selectField { display: flex; flex-direction: column; gap: 8px; font-weight: 700; flex: 1 1 180px; }
.selectField select, .pageControls select { background: $tc-2026-surface; border: 1px solid $tc-2026-border-strong; padding: 10px 12px; min-height: 40px; color: $tc-2026-body; font-weight: 400; max-width: 100%; }
.filterField { display: flex; flex-direction: column; gap: 8px; flex: 1 1 220px; min-width: 160px; }
.filterField label { font-size: 16px; line-height: 24px; font-weight: 700; }
.filterField input, .filterField select, .pageControls select { background: $tc-2026-surface; border: 1px solid $tc-2026-border-strong; padding: 10px 12px; min-height: 40px; color: $tc-2026-body; font-weight: 400; max-width: 100%; }
.filterField input, .filterField select { width: 100%; height: 48px; box-sizing: border-box; font-size: 16px; line-height: 24px; }
.filterField input::placeholder { color: $tc-2026-muted; opacity: 1; }
.filterField input:disabled { background: $tc-2026-border; cursor: not-allowed; }
.filterActions { min-height: 48px; }
.tableScroll { overflow-x: auto; width: 100%; }
.table { width: 100%; border-collapse: collapse; text-align: left; }
Expand Down Expand Up @@ -68,6 +71,6 @@
.headerActions { width: 100%; justify-content: space-between; }
.panelHeader, .filters, .pagination { padding: 16px; }
.panelHeader { align-items: flex-start; flex-direction: column; }
.search, .selectField { flex-basis: 100%; }
.filterField { flex-basis: 100%; }
.error { align-items: flex-start; flex-direction: column; }
}
11 changes: 1 addition & 10 deletions src/apps/sales/src/SalesPage.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import '@testing-library/jest-dom'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ButtonHTMLAttributes, ChangeEvent, ReactNode } from 'react'
import { ButtonHTMLAttributes, ReactNode } from 'react'

import SalesPage from './SalesPage'
import { SalesReport } from './sales.models'
Expand All @@ -19,15 +19,6 @@ jest.mock('~/libs/ui', () => ({
</button>
),
IconOutline: { RefreshIcon: () => <span /> },
InputText: (props: {
label: string; disabled?: boolean; name: string; value: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void
}) => (
<label htmlFor={props.name}>
{props.label}
<input disabled={props.disabled} id={props.name} value={props.value} onChange={props.onChange} />
</label>
),
LoadingSpinner: (props: { message: string }) => <span>{props.message}</span>,
PageTitle: (props: { children: ReactNode }) => <title>{props.children}</title>,
}), { virtual: true })
Expand Down
60 changes: 31 additions & 29 deletions src/apps/sales/src/SalesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/* eslint jsx-a11y/no-noninteractive-tabindex: ["error", { "roles": ["region"] }] */
import { FC, FormEvent, useCallback, useEffect, useRef, useState } from 'react'

import { Button, IconOutline, InputText, LoadingSpinner, PageTitle } from '~/libs/ui'
import { Button, IconOutline, LoadingSpinner, PageTitle } from '~/libs/ui'

import { SalesQuery, SalesReport } from './sales.models'
import { fetchSalesReport, salesErrorMessage } from './sales.service'
Expand Down Expand Up @@ -188,20 +188,21 @@ const SalesPage: FC = () => {
</div>

<form className={styles.filters} onSubmit={applyFilters}>
<InputText
classNameWrapper={styles.search}
forceUpdateValue
label='Search sales'
maxLength={200}
name='sales-search'
onChange={event => setSearch(event.target.value)}
placeholder='Search all report fields'
tabIndex={0}
type='text'
value={search}
/>
<label className={styles.selectField} htmlFor='sales-filter-column'>
Filter field
<div className={styles.filterField}>
<label htmlFor='sales-search'>Search sales</label>
<input
id='sales-search'
maxLength={200}
name='sales-search'
onChange={event => setSearch(event.target.value)}
placeholder='Search all report fields'
spellCheck={false}
type='text'
value={search}
/>
</div>
<div className={styles.filterField}>
<label htmlFor='sales-filter-column'>Filter field</label>
<select
id='sales-filter-column'
onChange={event => setFilterColumn(event.target.value)}
Expand All @@ -212,20 +213,21 @@ const SalesPage: FC = () => {
<option key={column.id} value={column.id}>{column.label}</option>
))}
</select>
</label>
<InputText
classNameWrapper={styles.search}
disabled={!filterColumn}
forceUpdateValue
label='Contains'
maxLength={200}
name='sales-filter-value'
onChange={event => setFilterValue(event.target.value)}
placeholder='Filter value'
tabIndex={0}
type='text'
value={filterValue}
/>
</div>
<div className={styles.filterField}>
<label htmlFor='sales-filter-value'>Contains</label>
<input
disabled={!filterColumn}
id='sales-filter-value'
maxLength={200}
name='sales-filter-value'
onChange={event => setFilterValue(event.target.value)}
placeholder='Filter value'
spellCheck={false}
type='text'
value={filterValue}
/>
</div>
<div className={styles.filterActions}>
<Button disabled={loading || !report} noCaps secondary type='submit'>Apply</Button>
<Button noCaps onClick={clearFilters} link>Clear</Button>
Expand Down
Loading