Skip to content

Commit 0af5fc1

Browse files
evereqclaude
andauthored
feat(ui): working read-only Views on every page + toolbar, filter, date, theme and selector fixes (#9975)
* feat(ui): finish the record View — every remaining placeholder button now shows a real read-only view The audit found 13 surfaces whose View button still opened the "we are working hard on this feature" placeholder (or did nothing at all). Each now opens the shared read-only surface introduced with ngx-record-view: Users, Organizations, Tasks (all three task pages via their shared component), Employee and Organization recurring expenses, Goal Settings (KPI + Time Frames), Inventory Merchants, Proposal Templates, Jobs Employees and Zapier Triggers/Actions get the right-side drawer; Sales Proposals routes to its existing read-only details page (whose duplicate Details button folds into View). Rules carried from the first wave: each View is gated by the record's own view permission (moved OUT of the *_EDIT blocks it used to hide in), drawers close when their list reloads, field descriptors mirror the grid columns and reuse existing i18n keys only. Also in the same directories: - Proposals statistics strip becomes three theme-token stat cards. - Jobs Employees "Actions" column: the bare green dot was a dead Nebular icon-font class; replaced with FontAwesome icons carrying translated titles, and the column header is translatable now. - Tasks grid: the ID column holds #GAU-7777 on one line and the Title column gets real width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): idle action toolbars no longer hold their hidden buttons' width The selection-action strip hid itself with translateX + overflow hidden, which removed it from VIEW but not from LAYOUT: the block kept the full strip's width (437px on Organizations, ~900px on Goals), so on button-heavy pages it could never fit beside the page title — the row wrapped, and the one visible "+ Add" button was left stranded mid-page, at a position that depended on the width of buttons nobody could see. The strip now collapses to zero layout width while disabled (animated max-width + opacity clip), so the toolbar fits on the title line on every page. Two supporting rules in the shared header layout: - the title block's flex basis goes to 0% so a wide title payload (the relocated breadcrumb trail, a secondary header button as on the proposals page) can never push the actions off the line — the title wraps inside its own block instead; - the actions block pins to the END of a wrapped line (margin-inline-start: auto) so on narrow viewports an expanded strip right-aligns instead of jumping to the left edge. Verified live on Organizations, Goals and Proposals: closed and open states both sit on the title/breadcrumb band, flush right. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): smart-table filter rows collapse behind a funnel toggle, with sort affordances Every list page carried a permanent row of filter inputs under the table header — a band of vertical space most visits never touch, built from widgets that did not even share a height. Three changes at the shared seam (no page template was touched; the directive attaches by element selector through SmartDataViewLayoutModule, the same route the settling skeleton took): - SmartTableFilterToggleDirective hides the filter row by default and injects a funnel button into the title row's last header cell. One app-wide preference persists the user's choice; a row with an ACTIVE filter is never hidden on load, and while filters are active the funnel carries an indicator dot so a hidden filter can never silently constrain the data. The stored preference is applied before first render — deciding it later flips a host binding mid-pass and throws NG0100 in dev mode. - The filter widgets get one compact metric (13px, 1.875rem controls) so inputs, nb-selects and ng-selects finally line up. - Sortable-but-unsorted column headers show a dimmed two-way arrow; the library only ever marked the active sort direction, so nothing told the user the other columns were sortable. e2e: the shared Playwright fixtures (plain + BDD) opt into always-open filters via the directive's stored preference, since several page objects type straight into the filter row. The static dev server now sends Cache-Control: no-store — dev bundles have unhashed chunk names, and the browser's heuristic cache kept serving a prior build's chunks after rebuilds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): table date cells show a short date; the full date moves to a tooltip CreatedAtComponent rendered the organization's long date format plus hh:mm:ss — "Wednesday, august 12, 2026 05:08:08 am" in every row — and DateViewComponent did the same without the time. Together they were the single biggest cause of horizontal scrolling on the task, payment and invoice grids. Both cells now render the locale-aware short form (moment 'll', e.g. "Aug 12, 2026") with the full org-format value on hover: created-at keeps date+time in its tooltip, date-view keeps the org's long date. The recurring-expense sync icon on date-view is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(theme): material themes join the shared gauzy token system The "Cosmic looks insane" report was actually Material Dark: its Nebular Material map paints header AND footer with color-primary (#e91d63 — Material pink) and, worse, neither material theme carried a single gauzy-* token, so every modern surface (cards, stat tiles, skeletons, table metrics, hover tints) rendered on EMPTY custom properties. The shared maps ($gauzy-density, action accents, action buttons) move out of themes.scss into _gauzy-theme-maps.scss — the material theme files register their themes before themes.scss's declarations run, so they could never merge maps that lived there. themes.scss forwards the partial, keeping every existing consumer working. Both material themes now merge the shared maps plus a canvas token set (the hand-tuned dark/corporate surface values), and their header/footer go surface-colored like every other registered theme — Material's pink and violet stay as ACCENTS. Verified live: under material-dark, --gauzy-card-2 resolves, the header paints #1f1f1f, and the pink survives only on primary controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): language selectors show a flag and the language name "AR (Arabic)" / "BG (Bulgarian)" becomes a small flag beside the plain language name, in both the shared ngx-language-selector and the Quick Settings selector (options and the closed trigger). Windows renders no emoji flags, so 13 tiny hand-written SVGs are vendored under assets/images/flags — one per LanguagesEnum language — resolved through one code→country map in @gauzy/ui-core/shared; a flag that fails to load hides itself, leaving the plain name. e2e follows the format change: language options no longer carry a locale-invariant "EN (" prefix, so the page objects now match options by the flag asset in their <img src>, and the trigger-caption assertions expect the native language names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): invoice line-item row actions render real icons, not bare dots The add/edit invoice tables injected '<i class="nb-plus/nb-checkmark/ nb-close/nb-edit/nb-trash">' into the smart-table action buttons — Nebular's icon font is long gone, so all five actions rendered as empty colored dots. Same fix as the job-employee grid: FontAwesome glyphs (loaded globally) with translated native titles, since these strings go through [innerHTML] where directives never bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: teach cspell the three new words from this wave animatable, macrotask and unhashed — all in code comments introduced by the record-View wave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): apply the PR review round — contrast, permissions, a11y and drawer hygiene CodeRabbit findings on #9975, all verified against a local build: - material-light: the header/footer band went surface-white but kept text tokens written for the old color-primary band — white on white. Both now read text-basic-color. - Users: the SUPER_ADMIN row gate was inverted twice over (a SUPER_ADMIN_EDIT holder got a DISABLED toolbar; a non-holder kept enabled buttons that silently no-op'ed on a cleared selection). A dedicated restrictedRow flag now gates the mutation buttons only, so the read-only View works under ORG_USERS_VIEW as intended. - Filter toggle directive: the delegated-listener marker is forgotten on teardown (a re-rendered row re-registers, keeping the indicator dot honest); nb-select values are detected via .select-button:not(.placeholder) (the host never carries the class); the funnel's aria-label/title re-translate on language change. - Action toolbar: an empty visible slot no longer paints a blank pill (:empty, with the slot content kept on one template line so no whitespace text node defeats it), and a fully open strip WRAPS on a viewport too narrow for it instead of clipping buttons out of reach. - Zapier drawers: dropped the raw-UUID row and its borrowed task-page label; the slug is the endpoint's real identifier. - Proposals stat cards: computed over the whole filtered set (same org/employee/date-range window as the table) instead of whichever page was loaded. - Invoice + job-employee smart-table actions: translated sr-only text inside the [innerHTML] strings names the links for screen readers. - Every drawer page closes its View when the table selection changes, so the drawer can never keep showing record A beside selected B. - type="button" on the three View buttons that could submit a form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: reword two directive comments cspell dislikes Comment-only; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): apply the second review round (cubic + greptile + CodeRabbit follow-ups) - Filter toggle: the accept/deny toggle filter is now DETECTED (its active choice button carries `.on`) and TRACKED (button widgets fire neither input nor change, so the delegated listeners gain click); row-scoped listener teardowns dispose per thead rebuild instead of accumulating for the directive's lifetime. - Action strip: the collapsed state is `visibility: hidden` (delayed through the closing animation), so keyboard users can no longer tab into clipped, aria-hidden buttons. - Tasks drawer badges: standard statuses de-slug ('in-progress' -> 'in progress') and the default badge class is `basic`, matching the grid renderer for OPEN and unlisted statuses. - Proposals stat cards: the aggregate query now applies the SAME column filters as the table, and a monotonic request guard stops an older response from overwriting a newer one; the card row wraps on narrow pages instead of overflowing the header. - Users: `restrictedRow` re-derives when role permissions change mid-session, not only on selection. - Recurring expenses (both pages): a reload drops the selection along with the drawer, so the toolbar cannot reopen a detached record. - e2e: the language page-data keys are named for what they now hold (flagEnglish: 'flags/gb.svg', ...). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs-ui): type-safe key-repeat check in the drop strip template Surfaced by a cold-cache build of this branch: 'repeat' does not exist on the template's Event typing for (keydown.space). Cast through $any(), same behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 066de81 commit 0af5fc1

83 files changed

Lines changed: 2458 additions & 542 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cspell.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,9 @@
545545
"lresult",
546546
"lyria",
547547
"macbook",
548+
"macrotask",
549+
"animatable",
550+
"unhashed",
548551
"MAINDB",
549552
"MAINLOADSPLASH",
550553
"MAINLOADURL",

apps/gauzy-e2e/src/support/Base/pagedata/ChangeLanguagePageData.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ export const ChangeLanguagePageData = {
88
English: 'Create',
99
Russian: 'Создать',
1010
Hebrew: 'צור',
11-
// Language-select option prefixes. Options render as "EN (English)" / "BG (Български)" etc.
12-
// (code + translated name) in a DB-driven order, so we pick options BY CODE PREFIX rather than by
13-
// the Cypress index (which assumed a fixed alphabetical order that the backend does not guarantee).
14-
codeEnglish: 'EN ',
15-
codeBulgarian: 'BG ',
16-
codeRussian: 'RU ',
17-
codeHebrew: 'HE '
11+
// Language-select option hooks. Options render as "[flag] Name" (flag <img> + translated name,
12+
// no code prefix) in a DB-driven order, so we pick options by the flag asset in their <img src>
13+
// — the only locale-invariant marker now that the code prefix is gone (language→country mapping
14+
// mirrors getLanguageFlagUrl in @gauzy/ui-core/shared).
15+
flagEnglish: 'flags/gb.svg',
16+
flagBulgarian: 'flags/bg.svg',
17+
flagRussian: 'flags/ru.svg',
18+
flagHebrew: 'flags/il.svg'
1819
};

apps/gauzy-e2e/src/support/Base/pagedata/SettingsButtonPageData.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ export const SettingsButtonData = {
22
// Light/Dark theme toggle now drives the body skin classes.
33
lightTheme: 'nb-theme-gauzy-light',
44
darkTheme: 'nb-theme-gauzy-dark',
5-
// Language options are labelled with a language-invariant 2-letter code
6-
// prefix (e.g. "EN (English)" / "EN (Английский)"), so we match on the code.
7-
languageEnglish: 'EN ',
8-
languageBulgarian: 'BG ',
9-
languageHebrew: 'HE ',
10-
languageRussian: 'RU ',
11-
// Resulting language-button captions after selecting each language.
12-
langButtonEnglish: 'EN (English)',
13-
langButtonBulgarian: 'BG (Български)',
14-
langButtonHebrew: 'HE (עִברִית)',
15-
langButtonRussian: 'RU (Русский)',
5+
// Language options render as "[flag] Name" (no code prefix); the flag <img src> is the only
6+
// locale-invariant marker, so options are matched on the flag asset path (language→country
7+
// mapping mirrors getLanguageFlagUrl in @gauzy/ui-core/shared).
8+
languageEnglish: 'flags/gb.svg',
9+
languageBulgarian: 'flags/bg.svg',
10+
languageHebrew: 'flags/il.svg',
11+
languageRussian: 'flags/ru.svg',
12+
// Resulting language-button captions after selecting each language: the trigger shows the
13+
// selected language's name translated in the just-activated locale, i.e. its native name.
14+
langButtonEnglish: 'English',
15+
langButtonBulgarian: 'Български',
16+
langButtonHebrew: 'עִברִית',
17+
langButtonRussian: 'Русский',
1618
// Layout dropdown options.
1719
layoutGrid: 'Cards Grid',
1820
layoutTable: 'Table'

apps/gauzy-e2e/tests/bdd/steps/change-language.steps.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { ChangeLanguagePageData } from '../../../src/support/Base/pagedata/Chang
77
// "+ Create" button text the app renders once that locale is active — both come from the page data,
88
// so QA can add a locale to the Examples table without touching this glue.
99
const languageCode: Record<string, number> = {
10-
Bulgarian: ChangeLanguagePageData.codeBulgarian,
11-
Russian: ChangeLanguagePageData.codeRussian,
12-
Hebrew: ChangeLanguagePageData.codeHebrew,
13-
English: ChangeLanguagePageData.codeEnglish
10+
Bulgarian: ChangeLanguagePageData.flagBulgarian,
11+
Russian: ChangeLanguagePageData.flagRussian,
12+
Hebrew: ChangeLanguagePageData.flagHebrew,
13+
English: ChangeLanguagePageData.flagEnglish
1414
};
1515
const translatedCreate: Record<string, string> = {
1616
Bulgarian: ChangeLanguagePageData.Bulgarian,

apps/gauzy-e2e/tests/support/bdd.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ export const test = base.extend<{ _bindPage: void }>({
1616
_bindPage: [
1717
async ({ page }, use) => {
1818
setPage(page);
19+
// Same opt-in as tests/support/fixtures.ts: filter rows are collapsed
20+
// by default for users, but the page objects type into them directly.
21+
await page.addInitScript(() => {
22+
try {
23+
localStorage.setItem('gauzy.smartTable.filtersOpen', 'open');
24+
} catch {
25+
/* storage unavailable — the spec will surface it */
26+
}
27+
});
1928
await use();
2029
},
2130
{ auto: true }

apps/gauzy-e2e/tests/support/fixtures.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ export const test = base.extend<{ _autoPage: void }>({
1313
_autoPage: [
1414
async ({ page }, use) => {
1515
setPage(page);
16+
// Smart-table filter rows are collapsed-by-default for users
17+
// (SmartTableFilterToggleDirective); several page objects type straight
18+
// into tr.angular2-smart-filters inputs, so specs opt into the
19+
// always-open behavior via the directive's stored preference.
20+
await page.addInitScript(() => {
21+
try {
22+
localStorage.setItem('gauzy.smartTable.filtersOpen', 'open');
23+
} catch {
24+
/* storage unavailable — the spec will surface it */
25+
}
26+
});
1627
await use();
1728
},
1829
{ auto: true }

apps/gauzy-e2e/tests/support/pages/ChangeLanguage.po.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ export const verifyLanguageOptionsVisible = async () => {
5151
};
5252

5353
/**
54-
* Pick a language option BY CODE PREFIX ("EN " / "BG " / "RU " / "HE ").
54+
* Pick a language option BY FLAG ASSET ("flags/gb.svg" / "flags/bg.svg" / …).
5555
*
56-
* The Cypress original selected by numeric index (Bulgarian=0, English=1, Hebrew=2, Russian=3),
57-
* assuming a fixed alphabetical order. The current app renders the option list in a DB-driven order
58-
* that is NOT guaranteed, so index selection is fragile — match on the language-code prefix instead
59-
* (the option text is "EN (English)", "BG (Български)", …). Robust and order-independent.
56+
* The Cypress original selected by numeric index, then the migration matched the "EN (" code
57+
* prefix. Options now render as "[flag] Name" with no code and a locale-dependent name, so the
58+
* flag <img src> is the one locale-invariant, order-independent marker left (see
59+
* ChangeLanguagePageData for the language→country mapping).
6060
*/
61-
export const clickOnLanguageOption = async (codePrefix: string) => {
62-
const option = getPage().locator(ChangeLanguage.languageOptionsCss).filter({ hasText: codePrefix }).first();
61+
export const clickOnLanguageOption = async (flagAsset: string) => {
62+
const option = getPage()
63+
.locator(ChangeLanguage.languageOptionsCss)
64+
.filter({ has: getPage().locator(`img[src*="${flagAsset}"]`) })
65+
.first();
6366
await option.click({ force: true, timeout: defaultCommandTimeout });
6467
// Selecting fires (selectedChange)->switchLanguage(), which re-renders the UI in the new locale and
6568
// collapses the sidebar. Give the translation swap a beat before the caller asserts on the button.
@@ -89,7 +92,7 @@ export const resetToEnglish = async (englishWord: string) => {
8992
await waitUntil(800);
9093
await getPage()
9194
.locator(ChangeLanguage.languageOptionsCss)
92-
.filter({ hasText: 'EN ' })
95+
.filter({ has: getPage().locator('img[src*="flags/gb.svg"]') })
9396
.first()
9497
.click({ force: true, timeout: defaultCommandTimeout });
9598
await expect(createBtn).toContainText(englishWord, { timeout: defaultCommandTimeout });

apps/gauzy-e2e/tests/support/pages/SettingsButton.po.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,25 @@ export const clickThemesDropdown = async (index: number) => {
4141
await waitUntil(1200);
4242
};
4343

44+
/**
45+
* Language options carry no locale-invariant text anymore (they render as "[flag] Name"), so
46+
* language entries in the page data are flag asset paths ("flags/gb.svg") matched against the
47+
* option's <img src>; every other dropdown (theme, layout) still matches by visible text.
48+
*/
49+
const dropdownOption = (match: string) =>
50+
match.includes('flags/')
51+
? getPage()
52+
.locator(SettingsButton.dropdownOptionCss)
53+
.filter({ has: getPage().locator(`img[src*="${match}"]`) })
54+
: getPage().locator(SettingsButton.dropdownOptionCss).filter({ hasText: match });
55+
4456
export const verifyTextExist = async (text: string) => {
4557
// The option list renders many nb-option nodes; assert the one matching `text`.
46-
await expect(
47-
getPage().locator(SettingsButton.dropdownOptionCss).filter({ hasText: text }).first()
48-
).toBeVisible({ timeout: defaultCommandTimeout });
58+
await expect(dropdownOption(text).first()).toBeVisible({ timeout: defaultCommandTimeout });
4959
};
5060

5161
export const clickDropdownOption = async (text: string) => {
52-
await getPage()
53-
.locator(SettingsButton.dropdownOptionCss)
54-
.filter({ hasText: text })
55-
.first()
56-
.click();
62+
await dropdownOption(text).first().click();
5763
await waitUntil(1500);
5864
};
5965

apps/gauzy-e2e/tests/support/pages/SettingsFeatures.po.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export const ensureEnglishLanguage = async () => {
2323

2424
// Already English? Nothing to do.
2525
const langBtn = page.locator(languageButton).nth(0);
26-
if (((await langBtn.textContent().catch(() => '')) || '').includes('EN ')) {
26+
// Trigger renders "[flag] Name"; when English is active the app locale is English,
27+
// so the caption is always the word "English".
28+
if (((await langBtn.textContent().catch(() => '')) || '').includes('English')) {
2729
return;
2830
}
2931

@@ -33,10 +35,11 @@ export const ensureEnglishLanguage = async () => {
3335
await page.locator(settingsBtn).first().click({ force: true });
3436
await page.waitForTimeout(1500);
3537
}
36-
// Open language dropdown (index 0) and pick the English option ("EN (...)").
38+
// Open language dropdown (index 0) and pick the English option by its flag asset —
39+
// the option text is locale-dependent now that the "EN (...)" code prefix is gone.
3740
await page.locator(languageButton).nth(0).click({ force: true });
3841
await page.waitForTimeout(1200);
39-
await page.locator(optionCss).filter({ hasText: 'EN ' }).first().click();
42+
await page.locator(optionCss).filter({ has: page.locator('img[src*="flags/gb.svg"]') }).first().click();
4043
await page.waitForTimeout(2000);
4144
};
4245

apps/gauzy-e2e/tools/serve-web.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function sendIndex(res) {
4646
res.end('index.html not found in ' + root);
4747
return;
4848
}
49-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
49+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
5050
res.end(data);
5151
});
5252
}
@@ -70,7 +70,9 @@ const server = http.createServer((req, res) => {
7070
return;
7171
}
7272
const ext = path.extname(filePath).toLowerCase();
73-
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
73+
// Dev bundles have UNHASHED chunk names; without this the browser's
74+
// heuristic cache keeps serving a prior build's chunks after a rebuild.
75+
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-store' });
7476
res.end(data);
7577
});
7678
});

0 commit comments

Comments
 (0)