mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
develop
14750
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fb5a40886 |
store page size between page loads for employees (#10319)
* store page size in localstorage * fix the reviews |
||
|
|
a73de1d121 |
Feat/add employee dialog polish (#10336)
* feat(i18n): add strings for the add-employee dialog and form sections * feat(user-forms): upload the profile photo through the shared avatar uploader * feat(user-forms): group basic info fields into profile, account and employment sections * feat(employees): track queued employees and the current entry in the add dialog * feat(employees): redesign the add-employee dialog with labelled steps, review list and fixed footer * style(employees): style the add-employee dialog header, steps, list and footer * feat(tags): draw the picker checkbox and show selected tags as tinted labels * style(tags): compact rows for the tag picker dropdown panel * fix(employees): vertically centre the Show Deleted toggle in the header * fix(employees): give Status and Screen Capture columns room from Name and Email * fix(employees): address review feedback on the add-employee dialog - mark template-only members protected - translate the step bar's aria-label - clear loading in a finally block so a failed create can be retried - fit the step bar on narrow dialogs by naming only the current step * fix(employees): resolve SonarCloud reliability issues in touched files - mark fire-and-forget promises with void (typescript:S9383) - drop async from delete(), which awaits nothing (typescript:S7503) * fix(user-forms): hide SUPER_ADMIN from the role picker once the permission check resolves excludeRoles() resolves after the role field may already have rendered its options, and it mutated the excludes array in place, so the field kept offering SUPER_ADMIN to users without that role. - basic info form: assign a new excludes array instead of pushing - role field: cache the fetched roles and re-filter them whenever excludes changes, clearing a selection that is no longer allowed * fix(user-forms): keep a preselected role until the role list has loaded applyExcludes() also runs from the excludes setter, which can fire before getAll() returns. With no roles loaded every preselected role looked disallowed and was cleared. Only check the selection once the roles have loaded; renderRoles() re-runs the check at that point. |
||
|
|
4c0d30c4e1 |
Feat/task details polish (#10329)
* feat(record-view): add opt-in wide input to the record drawer * feat(record-view): toggle the wide drawer class from the input * style(record-view): size and pad the wide drawer for long-form content * feat(tasks): render task descriptions from GitHub-flavoured markdown and safe HTML * feat(tasks): add task view component deriving pills, people and description * feat(tasks): lay out task view like an issue tracker with a collapsible details panel * style(tasks): style task view details, avatars and markdown content * feat(tasks): declare the task view component * feat(tasks): show the task view in the wide drawer * refactor(tasks): drop the record-view descriptor now served by the task view * feat(record-view): add markdown and status field types, section variants and row icons * feat(record-view): render panels, person chips, status and markdown in the shared view * feat(tasks): describe the task view through the shared record view * refactor(tasks): remove the separate task view component * refactor(record-view): split markdown block rendering into per-block renderers * refactor(record-view): derive avatar hue without bitwise truncation * fix(record-view): keep fenced code intact when stripping comments and split list parsing * fix(record-view): guard meta rows by permission and use inject() with class bindings * fix(tasks): honour done statuses when flagging an overdue task * fix(record-view): close code fences only on a matching fence and resolve Sonar findings in the markdown renderer * style(record-view): merge the duplicate wide drawer selector * fix(record-view): detect tilde fences, keep terminal # in headings and make placeholder restoration terminate * fix(record-view): strip placeholder marks from reference URLs and restore placeholders in one pass |
||
|
|
c12fc11a50 |
fix(auth): make email verification work end to end and stop register dead-ends (#10332)
* fix(auth): make email verification work end to end and stop register dead-ends
Verification link and notice
- /auth/confirm-email no longer sits behind NoAuthGuard. Registering signs the
user in, so the emailed link was opened by a signed-in user, redirected to the
dashboard before the resolver ran, and never verified anything. The token is
still required and still single use (the API clears it on success).
- A refused or expired link now shows its message instead of an endless spinner,
and a successful one marks the signed-in user verified in the store.
- New "verify your email" notice with Resend (POST /auth/email/verify/resend-link,
already throttled 3/min) in the main layout and on tenant onboarding. It shows
only when GET /auth/email/verify/status (new, same feature flag, so 404 where
verification is off) confirms the user is unverified.
- Settings > Billing: an unverified admin with no linked subscription is told a
paid plan connects once the address is verified.
Email sending
- Templates fall back to English when the recipient's locale has none, instead
of rendering an empty email (verification exists only in en/bg/he/ru).
- Send failures are logged with the provider code, SMTP reply code and command,
every address masked; verification sends are now recorded in email_sent with
status SENT/FAILED (subject only, never the link). A transport that fails
verification throws instead of returning undefined.
- resend-link answers 503 when the provider refused the message, not "OK".
- The verification link encodes the address (plus-addressing survived as a space).
- A caller-supplied appEmailConfirmationUrl is honoured only on an origin this
deployment serves (CLIENT_BASE_URL, the configured links, EMAIL_LINK_ALLOWED_ORIGINS;
"*" disables the check).
- Auth emails carry X-PM-TrackLinks: None so Postmark stops storing tokens as clicks.
- {{appLink}} falls back to CLIENT_BASE_URL when APP_LINK is empty: every
hosted deployment has APP_LINK empty, so welcome emails linked to localhost:4200.
Register
- A refused sign-up shows the API's 4xx message (e.g. "A subscription is
required...") instead of "Something went wrong", and the 403 checkoutUrl is
offered as a "Continue to checkout" button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auth): confirm-email trusts the API, not the link, for who was verified
Review follow-ups on the confirm-email page:
- After a successful confirmation, re-read GET /auth/email/verify/status
instead of comparing the link's email parameter with the signed-in user.
The token decides which account was confirmed; the email parameter is not
bound to it.
- A request that got no HTTP answer (status 0) is shown as a connection
problem, not as an invalid link.
- ActivatedRoute, Store and AuthService come from inject().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auth): apply a verification status only to the user it was asked for
If a different user signs in while the status or resend request is in flight,
the answer no longer updates the new user's verification state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui-core): drop a resend answer once another user has signed in
The verification notice now tracks the user it speaks for. A different user
signing in cancels the pending resend and resets its state, and a late answer
for the previous user changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui-core): a dismissed verification notice stays dismissed only for that user
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ba62a02001 |
fix(billing): scope Stripe linking, paywall and billing pages to this deployment's product (#10331)
Product-scoped (hosted plan only) Stripe webhook linking, signup paywall, lazy tenant link and /billing routes; checkout-session proof at register/onboarding; BILLING_PRODUCT, BILLING_SIGNUP_PAYWALL and BILLING_WEBHOOK_LINKING (default off); 402 payment_method_required on a paid upgrade without a card. See the PR description for details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
367a01a66d |
fix(desktop): use a single ErrorHandler that forwards to Sentry and ErrorHandlerService (#10326)
* fix(desktop): use a single ErrorHandler that forwards to Sentry and ErrorHandlerService The desktop, agent and desktop-timer apps registered two ErrorHandler providers. Angular only keeps the last one, so either Sentry never saw uncaught errors (desktop) or the app toast/logging handler was dead (agent, desktop-timer). Replace both with one factory provider that delegates to the Sentry handler and then to ErrorHandlerService. Sentry console logging is disabled since ErrorHandlerService already logs the error. Closes #9106 * refactor(desktop-ui-lib): extract provideGlobalErrorHandler to remove duplicated provider Move the combined ErrorHandler factory into desktop-ui-lib so the three desktop apps share one implementation. The reporting handler (Sentry) is passed in, so desktop-ui-lib does not need a Sentry dependency. * fix(desktop-ui-lib): always run ErrorHandlerService and guard nullish errors in global handler Run ErrorHandlerService in a finally block so a throwing reporting handler cannot skip it, and wrap nullish values in an Error before passing them to ErrorHandlerService. The value sent to the reporting handler is unchanged. |
||
|
|
a15f8536cd |
Feat/accounting dashboard polish (#10327)
* feat(i18n): add English strings for the accounting dashboard view * feat(dashboard): redesign the accounting dashboard with KPIs, cash-flow chart and sortable breakdown * fix(dashboard): keep employee chart legend items at the list font size * feat(i18n): add Acholi strings for the accounting dashboard view * feat(i18n): add Arabic and Hebrew strings for the accounting dashboard view * feat(i18n): add Bulgarian strings for the accounting dashboard view * feat(i18n): add German strings for the accounting dashboard view * feat(i18n): add Spanish strings for the accounting dashboard view * feat(i18n): add French strings for the accounting dashboard view * feat(i18n): add Italian strings for the accounting dashboard view * feat(i18n): add Dutch strings for the accounting dashboard view * feat(i18n): add Polish strings for the accounting dashboard view * feat(i18n): add Portuguese strings for the accounting dashboard view * feat(i18n): add Russian strings for the accounting dashboard view * feat(i18n): add Chinese strings for the accounting dashboard view * fix(dashboard): address accounting dashboard review findings - keep the signed income share for the label, clamp only the bar - reject non-positive total income as a percentage denominator - add a screen-reader table of the chart's daily values - let long KPI amounts wrap instead of truncating - format chart dates in the active UI language - translate the accounting view strings in Hebrew and Acholi * feat(dashboard): restore the cash-flow chart's day grid, point dots and full axis labels - draw a vertical grid line per date, in the value grid's colour - show a dot on every day for each series - label the y-axis with full figures and the x-axis with full dates * feat(dashboard): add a sort control to the earnings breakdown - add a sort-by select and a direction toggle to the breakdown header - sort by total expenses, highest first, by default - start-align the money columns and trail their sort arrows - narrow the employee column so the money columns get more room - add the sort label and direction strings in every locale * refactor(dashboard): inject the accounting view's theme service and currency pipe with inject() * fix(dashboard): address accounting chart and header review findings - keep a vertical grid line for every date; thin only the colliding labels - format y-axis figures in the active UI language - let the breakdown panel header and sort controls wrap on narrow screens * fix(dashboard): read the x-axis tick font through CartesianScaleOptions |
||
|
|
5c8d4e1c8c |
chore(cspell): declare or reword the words #10324 added
The develop push of #10324 (
|
||
|
|
e39e9323c6 |
test: finish the Angular stub specs; interop for callable CJS, d3 transforms
Local runs after this commit: ui-core 45/45 suites, ui-auth 4/4, gauzy 29/29, desktop-ui-lib 41/41, gauzy-server 2/2, and every plugin UI project that was red (integration-ai/github/hubstaff/upwork-ui, job-employee/ matching/proposal/search-ui, jobs-ui) green. - jest.interop.js now patches every callable CommonJS package the code imports both ways (moment, randomcolor) with `default` + `__esModule`, so both import styles resolve to the function under either esModuleInterop. apps/gauzy's spec tsconfig turns esModuleInterop on (as ui-core, docs-ui and videos-ui do): ui-core's line chart default-imports chartjs-plugin-annotation and Chart.register received undefined. - jest.preset.js transforms d3-* / internmap (ESM-only, reached through @swimlane/ngx-charts). - desktop-ui-lib specs: section forms get the FormGroup their parent passes, cell renderers their rowData, dialogs their data; Settings/Setup/ ScreenCapture get the library's GAUZY_ENV and the time tracker the AuthStrategy/AuthService the desktop apps bootstrap; the task table a signed-in session. Two test-side overrides are documented in place: LanguageSelectorComponent binds [(ngModel)] without FormsModule (a dead binding in the app too), and the plugin source forms use <nb-hint> as a plain styled element (Nebular has no such component). - apps/gauzy: page specs get the date-picker config their route sets, the timesheet layout / job pages their route data, the task dialog a selected organization (its unguarded async read crashed the Jest worker) and a stand-in for docs-ui's links panel (docs-ui's entry point loads a PDF viewer that uses import.meta, which CommonJS Jest cannot parse). - integration-upwork-ui: two hand-written specs used jasmine spies (jest has none) and partial Router/TranslateService mocks the template could not render with; they use jest.fn() and the real services now. - gauzy-server's AppComponent spec was the 2021 CLI scaffold (title 'desktop-web-ui', a "Welcome" h1) and declared a standalone component; it now imports it and asserts what the shell does (router outlet, language bridge started on init), with the library entry mocked to its one token. - jobs-ui: the layout spec has asserted a main landmark since it was written; the layout now renders <main role="main"> around its outlet (the app shell defines no other main landmark). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d3d8687ac1 |
test: app-wide TestBed defaults for the Angular projects; fix their stub specs (WIP)
Once the Angular suites loaded, most failures were CLI-generated `should create` stubs that had never run, failing on the first injection (TranslateService, NbToastrService, NbDialogRef, ActivatedRoute, the translate pipe, the default icon pack). - jest.angular-defaults.ts (setupFilesAfterEnv, after each project's test-setup): a top-level beforeEach adds what the app's root module provides - TranslateModule.forRoot, the Nebular forRoot modules, NgxPermissionsModule.forRoot, the Tabler/eva icon pack, HttpClient testing, router, noop animations, a NbDialogRef stub, GAUZY_ENV - and a matchMedia stand-in (jsdom has none). configureTestingModule merges, so a spec's own providers still win. It imports no ui-core/core: that would cache real modules a spec later jest.mock()s. - jest.preset.js maps dayjs/esm (imported by ngx-daterangepicker-material's .mjs bundle) to dayjs's CommonJS build. - Specs of non-standalone components import the NgModule that declares them, provide what the host feature module provides (EmployeesService, AuthService, NbAuthModule.forRoot, PipesModule), and set the inputs / route data / date-picker config the app always supplies before render. - Two stubs imported classes their files do not export (GauzyRangePickerComponent, ViewComponent) and declared `undefined`. - desktop-ui-lib's test-setup installs an inert `window.electronAPI` preload bridge of the shape apps/agent exposes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d0b2a8e080 | Merge remote-tracking branch 'origin/develop' into fix/unit-tests-hermetic-harness | ||
|
|
4c4e554e2b | Merge remote-tracking branch 'origin/develop' into fix/sentry-quota-flood | ||
|
|
91d91ffca3 |
test: make the unit-test run hermetic and fix the Angular jest harness
The Unit Tests workflow has never been green (24 of 97 projects red at
develop
|
||
|
|
fba9302a54 |
fix(sentry): Render asks for the DSN; desktop apps stop printing it
The Render blueprints declare SENTRY_DSN with sync: false, so Render prompts the person creating the Blueprint for their own DSN instead of shipping one. The desktop, desktop-timer, server, server-api and agent apps logged the full DSN at startup; they now only say that Sentry is enabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5536f7b711 |
fix(ui): show invoice View and Payments as active actions
Those buttons used the muted secondary style, so they looked disabled next to Edit and Download. Use the primary action style while they stay clickable. Fixes #8522 |
||
|
|
eac7136562 | fix unknown employee name in creation toast (#10306) | ||
|
|
0fe8348260 |
Merge pull request #10313 from joel-kalema/fix/screenshots-gallery-viewer-clean
Fix/screenshots gallery viewer clean |
||
|
|
e9e6bfd9fa |
Merge pull request #10308 from joel-kalema/feat/contacts-pages-polish
Feat/contacts pages polish |
||
|
|
69a293175a |
fix(sentry): only errors become Sentry events by default; stop logging health probes
The whole ever-co Sentry organisation (5,000 errors a month on the current plan) has accepted no error after the first hours of each monthly reset: 2026-07-09, 08-09 and 09-09 are the only days with accepted errors in 90 days. About 97% of what was accepted were info-level log lines, not errors. - SentryService (the API's Nest logger) turned every log/warn/debug/verbose call into its own Sentry event and ignored the `logLevels: ['error']` the API passes. It now honours `logLevels`: listed levels become events, the rest become breadcrumbs on the next event. An unset or empty list keeps the old capture-everything behaviour. The API reads the list from SENTRY_LOG_LEVELS (default `error`), so capturing warnings or logs again is a config change. - RequestContextMiddleware logged the start and end of every request, including the Kubernetes readiness probe on /api/health every 10 s on every pod: 2 events per probe, ~2,900 events an hour from the four Ever Teams API pods alone. /api/health and /api/health/* are no longer logged. This is decided by path only, since a client-set User-Agent must not be able to hide other requests. - apps/api/src/sentry.ts printed the DSN at startup and ran the SDK in debug mode in production (`environment.production` is false in the published image), writing several SDK lines per request. Debug is now opt-in with SENTRY_DEBUG=true, and the DSN is no longer printed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bc67c035da | fix unknown employee name in creation toast | ||
|
|
fb2184e41f |
Merge pull request #10305 from ever-co/fix/desktop-update-followups
fix(desktop): update-feed follow-ups: publish-channel CI guard, updater tag re-resolve, local-update note, stray channel |
||
|
|
4a1faad0f2 |
Merge pull request #10280 from aditya-mitra/confirm-dialog
remove duplicate confirm dialog when deleting proposal template |
||
|
|
21abafd1a3 |
fix(desktop): drop stray committed publish channel from apps/desktop/src/package.json
`"channel": "latest-x64"` is `pack --arch` output that was committed by
accident in
|
||
|
|
f0bce080a9 |
Feat/reports UI polish (#10289)
* feat(ui-core): add a shared stylesheet for the row-table reports
Apps & URLs, the weekly and daily limit reports and the two budget
reports are all built from one skeleton, but each page restyled that
skeleton on its own and the five had drifted apart: different row
heights, different type sizes, different numbers of nested panels.
report-table.scss holds what they share - the card, the hairlines, the
row metrics, the 12px type and the progress bar - behind a report-table()
mixin, plus a report-column() mixin for sizing a column from the row
rather than from whatever happens to be in it.
* refactor(ui-core): rebuild the apps & urls grid on the shared report table
Three levels of wrapper per row each painted their own gauzy-card-1
panel with 10px of padding above and below, so a row sat inside four
edges of one colour and ~40px of dead space separated the last row of
one project from the first row of the next. The surface is drawn once
by the card now and the grouping is carried by hairlines instead.
Two layout faults measured on the running app go with it:
* the card edge was a box-shadow ring, which paints 1px OUTSIDE the
border box. These cards sit flush against the left edge of
.report-container, which is overflow:auto with no left padding, so
the left-hand edge of every ring fell outside the scroll box and was
clipped. The right-hand edge survived on that container's 8px of
right padding, which is why the cards looked like they had three
sides. A border paints inside the box and cannot be clipped.
* overflow:hidden on the card changed its automatic minimum size from
min-content to zero, so flex was free to shrink the cards below
their own rows - and did: with four date groups in a .weekly-logs of
definite height every card was squeezed to about a third of its
content, and the overflow:hidden that allowed it also hid the cut.
* fix(ui-core): let the apps & urls column labels take their own font weight
The bootstrap font-weight-bold utility carries !important, which pinned
the column labels at 700 and made the 600 they are set in unreachable
from SCSS. align-items-center went with it: .table-inner-wrapper already
centres its items.
* refactor(gauzy): rebuild the time limit report on the shared report table
One component behind two routes, so the block that sizes the columns has
to serve both orders they come in.
The widths were plain width values, which a flex item ignores once it has
a basis, and they summed past 100% before the 10px margin report.scss
hangs off each column was counted - so every column was being shrunk, and
an xxl breakpoint then re-shrank two of them again to make room. As
flex-basis with the name column taking the slack nothing is shrunk, and
the breakpoint is gone.
The activity column used to take the remainder, which made the bar 640px
wide on a 1274px table - longer than the other four columns together, for
a value that is a proportion and reads no better for the length.
The three figure columns are sized by their HEADERS rather than their
values: every value is a 00:00:00 about 58px wide, but "Remaining hours"
needs ~118px in caps and was wrapping onto a second line, pushing the
header row taller than any row beneath it.
* fix(gauzy): drop a stray 8 from the daily limits spent hours heading
A literal 8 sat in front of the translated label and rendered as
"8 Spent hours".
Also drops the bootstrap font-weight-bold utility from the column header
wrapper, which carries !important and made the weight the stylesheet sets
unreachable.
* refactor(gauzy): rebuild project budgets on the shared report table
The Budget cell had three competing widths - flex-wrap: wrap, a 5% right
padding and a 65% progress wrapper - which added up differently at every
viewport, so the bar wrapped under the amount on some rows and sat beside
it on others, and the 5% left the column ending somewhere other than the
four columns above it.
Amount then bar on one line now, with the amount right-aligned in a box
of its own width so the bar starts at the same x on every row. Shrink
wrapped and left-aligned it was a different width per row - BGN 602.71
against 122:00:00 Hours - and walked the start of the bar back and forth
down the column.
The Budget column no longer takes the remainder either: that made the bar
grow with the window.
* fix(gauzy): sit the project budgets budget heading over its amounts
The Budget column holds two things, an amount and then a bar, so an
unwrapped label sat at the far left of the whole column - about 110px to
the left of the figures underneath it. Wrapping it in a span lets it be
given the width of the amount it heads.
Also replaces the literal string "No data yet...." in the
Employees/Teams column with the em dash this same template already
renders for a budget type it has no figure for.
* refactor(gauzy): rebuild client budgets on the shared report table
The same table as project budgets with a contact in place of the project,
so it is sized the same way and its Budget cell is rebuilt the same way:
the three competing widths become an amount right-aligned in a box of its
own width, then the bar. This copy also hung a 2% left margin off the
progress wrapper on top of the other three.
* fix(gauzy): sit the client budgets budget heading over its amounts
Wrapping the label lets it be given the width of the amount it heads,
instead of sitting at the far left of a column whose first 140px is the
amount and whose remainder is the bar.
Also replaces the literal string "No data yet...." in the
Employees/Teams column with the em dash this same template already
renders for a budget type it has no figure for.
* refactor(gauzy): rework the all reports page card grid
* category separation was 1rem, the same step as the gap between two
cards INSIDE a category, so the grouping the headings announce was
nowhere in the spacing. It is the larger of the two steps now.
* the card and preview edges come off the border ramp rather than the
card-shadow token, which is a grey-lavender line - that tint is what
made these cards read as coloured.
* the description is clamped to two lines and reserves the second. The
seeded descriptions run 60-90 characters against a minmax(20rem, 1fr)
track, so they wrapped to one line or two depending on the wording,
and that difference travelled down the card and put each row of
screenshots on a different top edge.
* the grid no longer centres its items. A grid item stretches to its
row height only while its own height is auto, so centring left every
card at its own content height about a common middle - which is why
one card's title did not line up with its neighbour's.
* a ::-webkit-scrollbar { display: none } rule is gone. Emulated
encapsulation scoped it to this component, so it hid the scrollbar on
the card body holding the whole report grid: the page still scrolled
several category rows deep with nothing on screen saying so.
* refactor(gauzy): rebuild the all reports card markup
The card was a div wrapping a flush nb-card with zero margin and zero
border, and the card already drew the shared card-shadow ring - so the
two hairlines landed on top of each other. It is one nb-card now, with
the screenshot in a preview tile of its own.
The view button was visibility:hidden until the card was hovered, which
took it out of the tab order: a mouse was the only way to open a report
from this page. It is always focusable now, and the overlay reveals it on
:focus-within so it is never an invisible target either.
* fix(gauzy): give the limit reports a real empty state
Two of them, and neither was designed.
A day with nobody on it rendered a bare div carrying bootstrap ml-3 - a
left margin and nothing else - so once the card body lost its padding the
line sat hard against the date above it with no vertical space at all. It
is a proper empty row now: the rule the column header would have drawn,
the gutter a row would have used, and the muted colour the column labels
use. The style lives in report-table.scss so the other row tables can
reach it.
It is deliberately not ngx-no-data-message: that is the WHOLE report's
empty state and draws a 48px icon at height 100%, so one per date card
would be louder than the data it stands in for, and a quiet week would
fill the page with chart icons.
The whole-report branch was a hand-rolled nb-card holding a bold centred
line - the only one of the five reports not using the shared empty state,
and the only one with no notion of loading, so it announced "No
Employees" on the first frame of every visit before the request had been
answered. It uses ngx-no-data-message now, which draws a skeleton until
the answer is in.
The spinner moves onto .table from a sibling div that only existed while
loading: with a skeleton below it the two would have stacked, and as an
overlay it now also covers a refetch, which the old sibling could not -
it was only rendered when the list was empty to begin with.
* feat(ui-core): extend the shared report table to accordions and badges
Two of the reports build a group from an `nb-accordion-item` rather than
an `nb-card` - Time & Activity and Manual time edits - but both spell it
with the same `.card` / `.card-title` classes, so the mixin now answers to
either element name. The accordion body also stops painting
`gauzy-sidebar-background-2`, which made an expanded group a panel inside
a panel exactly as the nested row wrappers used to.
`nb-badge` joins the type scale. `report.scss` sets it at 14px, which was
the row's own size when that rule was written; with the tables at 12 it
had become the single largest piece of text in the row, so an activity
percentage outranked the employee it belonged to.
* fix(ui-core): mute the project member count
`.employees-count` set no colour of its own, so it inherited
`text-basic-color` from the table row and came out at the same
full-strength white as the project name above it - two lines of equal
weight where only one is the subject.
It takes `gauzy-text-color-2`, the token the avatar chip's own `.caption`
already uses. The two sit side by side in every one of these tables, each
a title over a secondary line, so they now read as one pattern.
Fixed at the source rather than in the report stylesheets because all
eight call sites are report tables where that line is a caption: the two
budget reports, manual time edits, and the payment, expenses, daily,
activities and desktop recap grids.
* refactor(ui-core): put the last four shared report grids on the report table
Time & Activity, Expense, Payments and Amounts owed are built from the
same skeleton as the reports already converted, but each had restyled it
on its own: 14px cells against 12 everywhere else, bold mixed-case column
labels, and the stack of nested panels the shared mixin exists to undo.
Their COLUMN WIDTHS are deliberately left alone. Those are each report's
own proportions, and re-sizing them is a separate job from the type
scale - doing both at once would have put layout changes into pages whose
layout nobody has complained about.
The one exception is the daily grid's accordion header, which is not a
title but a summary ROW: its columns are its own direct children rather
than a `.table-inner-wrapper`'s, so it takes the row's padding and column
gap instead of the group-title padding every other report gets.
* refactor(gauzy): put weekly and manual time edits on the shared report table
The last two row tables that were still setting their own type. Same
treatment and same limit as the four shared grids: surface, hairlines,
row padding and the 12px scale, with each report's column widths left as
they were.
* fix(gauzy): render the manual time edit rows at all
The rows sat inside a bare `<ng-template class="table-template">`. An
ng-template with no structural directive and no `ngTemplateOutlet` is
never instantiated, so the `@if (dailyData?.length > 0)` branch rendered
nothing - and that is the branch taken whenever the report HAS data. The
page was blank exactly when there was something to show, and the only
thing it could ever display was the empty state in the `@else`.
Unwrapping the template is the whole fix. Behaviour, not styling, so it
is on its own: the report now shows its rows.
* feat(ui-core): give the report group header a surface of its own
The group label - a date, or an employee or project chip - sat on the same
surface as the rows beneath it, so it read as a first line of content
rather than as a band across the table.
`background-basic-color-2` rather than `gauzy-card-2`: the row hover
already uses the latter, and a header painted the same as a hovered row
would look like one. It is a step off the card in both themes without
introducing a third colour - rgb(24, 24, 27) on the dark card's
rgb(18, 18, 20), and #f7f9fc on the light card's white.
* feat(ui-core): let the activities grid take group by from its host
`groupBy` becomes a settable input backed by a getter/setter pair, so it
can be driven from the page while this component's own select still binds
it with `[(ngModel)]`, and a change from either side refetches through the
same subject the filters input uses.
`showGroupBy` turns off the control this grid draws for itself. Nothing
uses it yet; the Apps & URLs page does in a following commit, to put Group
on one line with the other filters instead of on a row beneath them.
* feat(ui-core): make the activities grid group by control optional
Wraps the control in `@if (showGroupBy)`, which defaults to true - so
every existing use of this grid renders exactly as before.
* feat(gauzy): move group by onto the apps & urls filter line
Group used to sit on a row of its own beneath the filters, as a second
band of chrome above the table. The page owns the value now and projects
the control into `ngx-gauzy-filters`, whose `<ng-content>` is the first
item inside its own filter row - so Group lands FIRST on the line, ahead
of the time zone, Source, Activity Level and Log Type, and the grid is
told not to draw its own.
The filter row and its column are also classed, so the stylesheet can
widen that row specifically in the commit that follows.
* fix(gauzy): keep the apps & urls filters on one line
The filter strip was pinned at 972px whatever the window did - measured
identical at a 1600px viewport and a 2400px one - so the fifth control
wrapped on a card 1570px wide with hundreds of pixels going spare.
The cap is four nested bootstrap boxes, every one of them shrink-to-fit:
.row > .col-auto.ml-auto (this template)
ngx-gauzy-filters
.row.col-auto.main-wrapper (that component)
.col-auto.ml-auto
.filter-item-list
A `.col-auto` is `flex: 0 0 auto`, so it takes its content's width rather
than its parent's, and each level also charges 15px of gutter either side.
Each box now takes the full width of its parent instead; the strip reaches
1248px and the controls stay right-aligned within it. Nothing is made
wider than the card - the cap is simply no longer narrower than it.
Also:
* Source, Activity Level and Log Type go from 220px to 185px. Two rules
held that width, `nb-select { width: 100% }` and a 220px `min-width`
on the wrapper, so narrowing the select alone left the old box around
a smaller control. No label truncates - the longest needs ~150px.
* every item loses its 5px bottom margin and the row centres instead.
The existing filters carried that margin and the projected Group
control did not, which under `align-items-end` left it sitting 5px
low. Equal boxes hold even if a control changes height later.
* wrapping is kept as a safety net over `report.scss`'s
`nowrap !important`: it cannot trigger at any normal width now, but if
something does take the room - the AI assistant sidebar is 384px - the
controls move to a second line rather than off the right edge.
* fix ed the select colors
* fix(ui-core): pack report table columns to the start
`report.scss` sets `justify-content: space-between` on the row, which
hands every column an equal share of whatever width is left over. On the
tables whose columns are all fixed that pushed the last one hard against
the right edge and blew the gaps between the rest out to roughly 95px, so
a row read as a handful of scattered values rather than as a set of
columns.
The gap between two columns is now always the shared column gap and any
slack is left at the END of the row. The header and the body are laid out
by the same rule, so they stay on one grid either way - re-measured at 0px
column drift across every row of all five table reports.
* fix(gauzy): read the limit report figures from the start of their column
Spent, Remaining and Limit were right-aligned, so their values hung off
the right edge of each column while the label above sat elsewhere and
nothing in the row began on a common edge.
All three now read from the start, headers included. The digits still
stack character for character: `tabular-nums` from the shared mixin is
what does that, not the alignment.
* fix(gauzy): align the project budget figures and their headings
Two faults in the same column block.
Spent, Remaining and the Budget amount were right-aligned, so a value hung
off the right edge of its column while the label above it sat elsewhere.
They read from the start now, headers included; `tabular-nums` from the
shared mixin is what keeps the digits stacked, not the alignment.
The Budget heading was also 42px off the amount beneath it despite both
sitting in an identical 140px box. The cause was
`@include nb-ltr(text-align, right)` - that mixin emits a `[dir=ltr]`
prefixed selector which did not match here, so the declaration silently
never applied and the heading kept the default alignment while the amount
did not. The last of those calls is gone from this file; a logical
`text-align: start` resolves per writing direction with no prefix to fail.
Measured after: header text and value text share a left edge to 0px on
every column, across all 16 rows.
* fix(gauzy): align the client budget figures and their headings
The same two faults as the project budget report, fixed the same way:
Spent, Remaining and the Budget amount read from the start of their column
rather than hanging off its right edge, and the Budget heading drops the
`nb-ltr(text-align, ...)` call whose `[dir=ltr]` prefixed selector never
matched - which had left it 30px off the amount below it.
Measured after: 0px between header text and value text on every column,
across all 8 rows.
* fix(gauzy): mark the all reports preview images as decorative
The `<img>` carried no `alt`, so assistive technology falls back to
announcing the image URL in its place. The tile is a screenshot of the
report the card already names in its header, which makes it decorative:
an empty `alt` is what takes it out of the accessibility tree rather than
putting a filename into it.
* fix(gauzy): reveal the all reports view action on touch devices
The View Report button is tappable and focusable while its overlay sits
at `opacity: 0` - it is `:hover` and `:focus-within` that bring the
overlay up. A touch screen has neither of those to offer, so the action
was there but unlabelled and invisible, and the card read as a static
screenshot.
`hover: none` matches exactly those devices - a primary pointer that
cannot hover, which no desktop browser reports - and shows the overlay up
front for them. The hover and focus rules are untouched everywhere else.
Because the overlay is permanent on those devices, the button is anchored
to the bottom of the tile and the gradient's dark end kept to the lower
half, so the screenshot the tile exists to show stays readable underneath
it.
* feat(ui-core): let a host widen and compact the filters strip
Every box `ngx-gauzy-filters` renders is a bootstrap `.col-auto`, i.e.
`flex: 0 0 auto`, so the strip is shrink-to-fit at each of its three
levels: a host that gives it a wide container still gets a strip the
width of the controls inside it. A host that wanted otherwise had no way
to ask for it, and the boxes involved are private to this component -
`::ng-deep` was the only route to them, which the repository's Angular
guide discourages.
Two opt-in inputs instead, reflected onto the host element so this
component's own stylesheet is what keys off them:
* `fullWidth` spans the container, keeps the controls against the
trailing edge, and lets them wrap to a second line rather than
overflow;
* `compact` narrows each filter column, for a host fitting more onto
that line than this component renders on its own - one projecting an
extra control through `<ng-content>`, for instance.
Both default to false, and nothing under either class is reachable
without it, so every existing consumer renders exactly as it did.
* refactor(gauzy): style the apps & urls filter strip without ng-deep
The strip's layout came from a `:host ::ng-deep .filters-col` block
reaching into `ngx-gauzy-filters`' `.main-wrapper`, `.filter-item-list`
and `.filter-item`. Those are the filters component's own boxes, and it
now carries `fullWidth` and `compact` inputs for exactly this - so the
template asks for the layout it needs and the rules live beside the
markup they describe. This page keeps only its own `.col-auto`.
The Group By control needed no API at all. It is declared here and only
projected, so it carries this component's encapsulation attribute
wherever it lands in the filters template, and is styleable directly.
Its select was the one genuinely cross-boundary case, since
`.select-button` lives inside `nb-select`'s own template. That is a token
redefinition now - `--select-min-width: auto` on `.group-by-filter` -
matching the pattern already used for the employees table toggle.
It also fixes the override rather than just relocating it: the old
`::ng-deep` declaration carried no `!important`, so it lost to
`.nb-theme-default nb-select .select-button { min-width: ... !important }`
in `_overrides.scss`, and the control was narrow on every theme EXCEPT
the default one. A token is read through that `!important` rather than
fighting it, so the width is now the same on all of them - which means
the default theme's Group By select is narrower than it was.
* fix the text alignement
|
||
|
|
5de84b3b25 |
Fix/date selector unit per route (#10275)
* fix(ui-core): a locked date picker owns its unit — a carried-over unit_of_time no longer wins
`isLockDatePicker` means the page works at ONE granularity: a day page (Time &
Activity, Screenshots, Videos, Apps, Visited Sites, Daily timesheet, Dashboard →
Teams) shows the activity of a single date, a weekly page a whole week, Approvals
a month. The user cannot change that unit there — `createDateRangeMenus` offers
only that unit and `showCustomRangeLabel` is off — yet the resolver let a
`unit_of_time` left in the URL by a PREVIOUS page override the route's own.
So arriving from any week page resolved a WEEK range for a day-locked route. A
carried-over `date_end` made it worse: it stretched the range across seven days,
and the arrows then stepped a week at a time, because the `isCustomDate` branch
of the arrow strategies measures the span of the range itself.
A locked route now derives both ends from the anchor date at its own unit and
ignores `date_end` and `unit_of_time` entirely. Unlocked routes — Reports, the
aggregate dashboards, Invoices, Payments, Time Off — still read the URL, so a
unit the user picked from the ranges menu survives a reload.
Also made the route config null-safe: a route that declares no `datePicker` used
to throw on `.unitOfTime` (reachable from the estimates list route) and now falls
back to DEFAULT_DATE_PICKER_CONFIG. `isCustomDate` keeps its effective meaning —
`parseToBoolean` never returns nullish, so the old `?? !!date_end` was dead code,
and reviving it would have marked every reloaded range custom since the picker
writes `date_end` for predefined ranges too.
* fix(gauzy): fold the resolved unit of time into the date picker config
The resolver is the only place that sees a route's picker configuration and the
URL together, and it runs exactly once per RESOLUTION — which makes it the right
owner of the effective unit for a page: the route's own unit where the picker is
locked, the URL's `unit_of_time` where the user is allowed to change it.
It already returned that unit on the resolved range, and the app component threw
it away, rebuilding the config from the static route data alone. The header
picker was left to re-derive the unit from the URL itself, racing the config it
was handed on NavigationEnd.
Folding `dates.unitOfTime` into the config makes the config the single source of
truth and lets the picker stop reading the URL as an authority (next commit).
* fix(ui-core): the route config outranks the URL for the picker's unit of time
Closes the feedback loop that left Employees → Time & Activity showing a single
date in the input while WEEK was the selected range.
The picker let the URL's `unit_of_time` outrank the route's `datePicker.unitOfTime`
unconditionally. Under `location.replaceState` that was harmless: the picker's own
writes produced no `queryParams` emission, so the URL only ever carried a unit some
route had put there. Once
|
||
|
|
6c550da567 |
Merge pull request #10281 from joel-kalema/fix/i18n-cache-headers-and-missing-keys
Fix/i18n cache headers and missing keys |
||
|
|
6ceaccc4db |
Fix/custom dashboard drag and drop (#10269)
* fix(dashboard): assign both grid axes when re-flowing a canvas layout `packLayout` only ever corrected `y`, leaving `x` exactly where it was. The canvas derives reading order from `(y, x)`, so reordering two widgets that shared a row was a silent no-op: the sort read the untouched `x` values back and restored the original order. `flowLayout` assigns both coordinates from the order of the array, which is what the builder actually models -- widgets are appended, reordered by drag and resized from a menu, never placed at an arbitrary column. It is the sparse packing algorithm from CSS Grid 8.5 step 4, because the canvas now renders its cells with spans only and lets the browser place them; the two have to agree or the persisted `x`/`y` would describe a layout nobody sees. Verified against a real browser across four layouts, including ragged heights. Sparse rather than dense on purpose: dense back-fills earlier gaps, which would let a widget render before one that precedes it in the array, and the index CDK reports for a drop is a position in that array. Also adds `readingOrder`, and the pure drop-point geometry (`isPointInRect`, `dropIndexAtPoint`) the canvas uses to resolve a release that landed between cells. `packLayout` is untouched: `normalizeLayout` still depends on it. * test(dashboard): cover canvas flow layout and drop-point geometry Locks in the two contracts that are easy to break without noticing: - `flowLayout` must pack exactly like CSS grid auto-placement, because the browser is what actually positions the cells. The ragged-heights case is the one that catches drift -- `d` tucks in beside the tall `a` at column 4 rather than starting a fresh row. - A move between two widgets on the same row must survive the reading-order sort that follows it. That is the regression that made drag-and-drop look dead. Plus the drop-point geometry: below everything, in a gutter, on the next row, off either edge, and the half-open edge rule that stops two neighbouring cells both claiming a boundary pixel. * chore(i18n): add widget height labels and update the builder drag copy `HEIGHT` / `HEIGHT_ROWS` back the new height ladder in the widget menu. The drag copy no longer points at the grip: the whole card is the drag surface now, and the rail is the keyboard control, so the hint and the handle label say that instead. Only `en.json` carries these -- the rest of the builder strings are English-only too, and the other locales fall back. * fix(dashboard): make canvas drag-and-drop actually move widgets Dragging a widget did nothing at all, in any direction. `cdkDropListOrientation="mixed"` selects CDK's `MixedSortStrategy`, whose `sort()` shows the new position by MOVING THE PLACEHOLDER NODE between cells (`overlapElement.after(current)`) and does nothing else. Every cell was pinned with `grid-column: x+1 / span w`, so DOM order had no bearing on where anything rendered: CDK was reordering nodes inside a grid that ignored node order. Measured in a browser -- moving the first cell to the end left every widget's pixel position identical. Cells now declare spans only and the grid auto-places them in DOM order, so the sort is visible and the placeholder shows the real destination. Three further fixes: - `cdkDragHandle` is gone. With it, only the 16px rail could start a drag and grabbing the card -- what everyone tries -- did nothing. The whole cell is the drag surface; the kebab still opens on click because CDK needs 5px of movement before treating a press as a drag. The rail stays as the arrow-key control, which is the only way to rearrange without a mouse. - A release over the canvas' empty space is resolved from the drop point. CDK only re-sorts while the pointer is over another cell, so dropping into a gap, the ragged space beside a tall widget, or the run-off below the last row silently kept the index of the last cell crossed -- dragging a widget to the bottom put it back near where it started. - A release off the canvas changes nothing. CDK leaves a rejected or stray item at whatever index it sorted to on the way past, so flicking a widget towards the palette (which refuses it) quietly reordered the canvas. The canvas rect is measured in the handler rather than read from `isPointerOverContainer`, which CDK answers from a rect cached at drag start -- a layout shift mid-drag leaves it tens of pixels stale and rejects drops made well inside. Also drops the dead `_normalizePlacementHeight`, which was a no-op today but would have squashed any widget declaring `minSize.h === 1` to one row on load, and swaps CDK's default preview (a full clone of the cell -- a large translucent card covering the slot being aimed at, with a blank cloned canvas) for a chip. Verified with a real CDK harness driven by Playwright: 17 scenarios covering horizontal, vertical up and down, to the start and end, ragged rows, gaps, dead space, palette drops, rejected drops, no-op drags and a scrolled page. * fix(dashboard): stop canvas rows inflating and tighten the grid `align-content` behaves as `stretch` on a grid and `grid-auto-rows`' `auto` maximum is stretchable, so the rows expanded to fill `min-height`: a single two-row widget on a fresh canvas grew to the full 14rem. `align-content: start` keeps rows at their own size, and the space left over becomes the run-off the drop-point fallback reads as "put it at the end". `min-height` now applies only while editing, where it is that drop target. A read-only canvas has nothing to drop, and reserving 14rem under two small widgets was just empty page. Density: the row unit goes 64px -> 52px and the gap 1rem -> 0.75rem, so a six-row widget is 372px rather than 464px. Widget bodies scroll, so a tighter unit costs a little scrolling inside a big widget and buys the whole canvas fitting on one screen. The grab rail is visible from the moment edit mode starts -- at `opacity: 0` the only affordance for rearranging was hovering a strip nobody knew was there -- and the cell reserves a gutter for it so it sits beside the card instead of over the first 14px of its content. Drops the sibling-transform transition: the mixed sort strategy re-orders nodes and never transforms siblings, so it was dead. * fix(dashboard): auto-scroll the dashboard while dragging a widget This host element is the dashboard's scroll surface -- `dashboard.component.scss` gives `router-outlet ~ *` the `overflow-y: auto`, because the app layout's own scroller is clamped so the header and footer stay put. `CdkDropList` only auto-scrolls the viewport and ancestors registered with `ScrollDispatcher`, which is what `cdkScrollable` does. Without it, dragging a widget towards the bottom of a canvas taller than the window stopped at the fold: the page never scrolled, so the lower half was unreachable by drag. * style(dashboard): match the page spacing to the canvas grid The page stacked its heading, hint bar, tab strip and body on a 1rem rhythm wrapped around a 0.75rem grid, which read as a loose frame round a tight canvas. Same gap on both now, and the heading loses a quarter rem it was not using. * feat(dashboard): show a drop slot when dragging a widget from the palette CDK carries the placeholder into whichever list the pointer is over, so the default one -- a clone of the palette row -- was being dropped into the canvas GRID as a full-width list entry. The slot is styled in the palette's own sheet rather than the canvas': CDK renders it from this component's template, so it carries this component's style attribute and a canvas-scoped rule would never match it. Its grid spans are inert in the palette's own block list and match the most common widget footprint, since the real one is only known once the drop resolves the widget's `defaultSize`. * feat(dashboard): let a widget's height be resized from its menu The menu offered width only, so a widget arrived at whatever `defaultSize` its author picked and the only way to stop one eating half the canvas was to remove it. `resized` now carries either dimension and the menu shows two ladders. A ladder rather than every value in range -- a chart allowing 3 to 10 rows would put eight near-identical buttons in the menu -- but both of the widget's declared bounds are always included, so the shortest and tallest size it supports are always reachable. * style(dashboard): tighten widget card chrome The header and body paddings are the frame's fixed overhead: paid by every card on the canvas at every size, and they consumed around 2.4rem of a two-row widget before its content got a pixel. Tight here so the grid can be tight. Also renames the menu's width ladder classes to serve both axes. * fixed the AI comments * fixed the AI comments |
||
|
|
5fafd072af |
Merge pull request #10260 from joel-kalema/feat/polish-estimate-dialogs-and-tables
Feat/polish estimate dialogs and tables |
||
|
|
ec1f6d3f7f |
Merge pull request #10255 from joel-kalema/feat/polish-key-result-dialogs
Feat/polish key result dialogs |
||
|
|
b781271668 |
Merge pull request #10236 from joel-kalema/feat/polish-goals-dialogs
Feat/polish goals dialogs |
||
|
|
ebf676b2f8 |
fix(security): No published default signing secrets (GHSA-39j7, GHSA-chm8, GHSA-4r2r, GHSA-3cgp) (#10240)
* fix(security): never sign tokens with a published default secret
GHSA-39j7-x845-4w3c (high) and GHSA-chm8-2ggf-pgjq: when JWT_SECRET,
JWT_REFRESH_TOKEN_SECRET, JWT_VERIFICATION_TOKEN_SECRET or
EXPRESS_SESSION_SECRET was unset, the API fell back to a literal published in
this repository, so anyone could forge tokens and sessions. resolveSecret() now
substitutes a per-process random value instead, and the startup guard still
reports such a secret as unset, so production keeps refusing to boot. The
known-default list is shared by the config resolver, the startup guard and the
desktop apps, and it catches a default published for any key, not only its own.
Desktop apps generated no secrets at all: every install shipped the same baked
DESKTOP_JWT_* values while binding the local API to 0.0.0.0. They now provision
random per-install secrets on first run and rotate a stored published default.
DEMO=true keeps today's behaviour, with a TODO, pending a decision on
demo.gauzy.co's secrets.
Also: the MCP refresh grant re-checks the account on every refresh instead of
trusting a 30-day-old token (GHSA-3cgp-wmrg-4fqg residual), and the misleading
comment claiming the Electron seed-credential exemption is harmless is corrected
(GHSA-4r2r-mv32-3468).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(config): resolve signing secrets lazily so load order cannot split them
Found by a runtime probe against a locally built API: login returned 200 but the
access token it issued was rejected on the next request, because the process was
signing with one secret and verifying with another.
apps/api/src/main.ts calls loadEnv() (which reads .env.local and friends) only
after its imports have run, so @gauzy/config can be evaluated while JWT_SECRET is
still unset. With the published literal as the fallback that was invisible: the
early copy and any later copy both ended up on 'secretKey'. Once an unset secret
became a per-process random value, the early copy generated one while a copy
imported after loadEnv() read the configured value — so tokens signed by one
never verified in the other, and every authenticated request 401'd.
The four secrets are now getters on `environment` / `environment.prod` /
`defaultConfiguration.authOptions`, so each is read at first use, after the env
files are loaded. Adds the regression test for exactly that order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(security): separate an unavailable account lookup from a dead refresh token
Review pass over PR #10240. The substantive fixes:
- MCP refresh grant (CodeRabbit major, Greptile P1). `getMcpUserInfo` caught
every error and answered `null`, so a database blip reached
`refreshAccessToken` as "this user is gone" and the grant handler answered
`invalid_grant`. Well-behaved OAuth clients discard a refresh token on that,
so one transient outage signed active users out for good. `getMcpUserInfo`
now re-throws a failed lookup, `refreshAccessToken` re-throws it as
`UserLookupUnavailableError` (outside its own catch, so it cannot be
flattened into `null`), and the grant answers `temporarily_unavailable` /
503. `null` now means exactly one thing: the account can no longer sign in.
The refresh token is still never revoked on either path.
- `resolveUser` is now REQUIRED (Greptile P2). It was optional on a public
method, so a caller could keep the two-argument shape and silently skip the
account check the parameter exists to perform. A missing resolver now throws
before anything else (500 server_error), matching the missing-provider path.
`UserLookupUnavailableError.is()` is used instead of bare `instanceof`: this
package ships both as source and as a bundle, and a downlevelled
`extends Error` would break the prototype chain and quietly restore the
invalid_grant behaviour.
- Nested credentials reached the desktop logs (CodeRabbit major, CWE-532).
`redactSecretsForLog` only looked at top-level keys, and `apps/desktop` logs
the whole `DesktopSetupConfig`, so `postgres.dbPassword` and
`secureProxy.ssl.key` were printed verbatim. It now walks nested objects and
arrays, with a depth cap and cycle detection.
- The seeded-account warning missed renamed installs (CodeRabbit major).
`getPublishedSeedAccounts()` read only today's `DEMO_*_EMAIL` values, but the
database was seeded in the past: an operator who changed the address after
installing still had `admin@ever.co` with the published password, and the
check walked past it. Both the configured and the canonical addresses are now
checked, deduplicated on the (email, password) pair.
- The bounded read could hide a vulnerable account (Greptile P2). One shared
`IN (...)` query with a global limit of 10 let the rows of whichever address
came back first use the whole budget. The budget is now per address (5 rows
each), so every candidate is actually looked at, and rows are re-matched
against the address they were fetched for.
- The lazy-getter commit (
|
||
|
|
a472bfa248 |
fix(security): CSV formula injection + rate-limit residuals (GHSA-7xp5, GHSA-86mw) (#10243)
* fix(security): neutralize spreadsheet formulas in CSV exports GHSA-7xp5-j564-4752 (medium): exported cells were written verbatim, so a stored value beginning with = + - or @ executed as a formula when a colleague opened the export in Excel or Sheets. A shared encoder in @gauzy/utils prefixes an apostrophe to any cell starting with a formula trigger (including the full-width forms), leaves strictly numeric values alone, and is reversed on import so an export/import round-trip stays byte-exact. Every field is now quoted, which also stops a bare CR inside a value from starting a new spreadsheet row, and the invoice CSV in the web app gets real RFC 4180 quoting instead of JSON.stringify. GHSA-86mw-2crg-vmhc residuals (low): the public invite routes are throttled, the shipped compose files no longer trust a forwarded client IP while publishing the API port directly, and RequestContext.currentIp() resolves the client address the same way the throttler does instead of reading a spoofable header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(export-import): decode CSV cells only for archives we marked The import side reversed the spreadsheet-formula escape on every parsed row, but an uploaded ZIP is not necessarily one this server wrote: it can be a dump from an older Gauzy, a filled-in `/export/template`, or a CSV set built by external tooling. For those, un-escaping is data loss — a legitimate value such as `'=notes` was persisted as `=notes`, silently. Both review bots flagged this as the one thing blocking the merge, and they were right: the decoder had no way to tell "we escaped this" from "somebody else wrote this". A data export now carries a `gauzy-export.json` marker at the archive root (format, version, `spreadsheetSafeCells`), written by `exportTables` and `exportSpecificTables`. `ImportService` resolves that marker once per import and decodes rows only when it is present and recognized; anything else is imported byte for byte as it was parsed. `/export/template` is deliberately NOT marked — an operator fills it in by hand, so nothing in it was ever escaped. The manifest reader is defensive about an attacker-supplied file: missing, oversized, malformed, a foreign format or a newer version all mean "do not decode". The invoice/payment CSV builder no longer has a path that skips encoding: a pre-joined header line used to be written through verbatim, and it was the only value in the file that reached disk unquoted and un-neutralized. `buildCsv` and `generateCsv` now declare `headers: string[]` (both callers already pass one), and a stray string from an untyped caller is split and encoded rather than trusted. Tests: `import.service.spec.ts` imports the same escaped CSV with and without a marker and asserts the apostrophe survives when unmarked (reverting the gate fails those 5 tests); `export.service.spec.ts` asserts the data archive carries the marker and the template archive does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cf438f50b8 |
feat: allow employees to see their tracked data by default (#9874) (#10212)
Adds an organization setting `allowEmployeeToSeeTrackedData` (default true, so existing organizations keep today's behaviour) that lets an admin hide an employee's own tracked data — screenshots, activity, app/URL history, time logs, videos and statistics — from that employee. - `EmployeeTrackedDataGuard` enforces it on the read routes of the activity, custom-tracking, statistic, time-log and time-slot controllers and of the videos, camshot and soundshot plugins. It depends only on the global DataSource, so plugins can use it. - Exempt: admins (`CHANGE_SELECTED_EMPLOYEE`), callers with no employee record in the tenant, team/project managers of the organization, and the routes recording depends on — `time-slot/:id`, `time-log/:id`, `time-log/conflict`, `POST statistics/tasks` (the desktop timer's task picker) and every write route. A spec pins that list. - The setting is read from the caller's own organization plus any organization the request names, so a client-chosen `organizationId` cannot bypass it, and the employee identity is read from the database rather than the token claim. - `GET /timesheet/statistics/tracked-data-access` lets the web UI hide navigation with the same rule; the menus, the activity tabs and the dashboard widgets follow it, managers included. - Adds the admin toggle, seeds, the SQLite/PostgreSQL/MySQL migration and i18n for all 14 locales, and removes a UTF-8 BOM from 11 locale files that broke locale loading in the desktop apps. Resolves #9874. Co-authored-by: Meet Prajapati <meet987654@users.noreply.github.com> |
||
|
|
33d464dba1 |
Merge pull request #10227 from joel-kalema/fix/polish-goals-pages
Fix/polish goals pages |
||
|
|
d810c81cf3 |
feat(core): persistence invariants, config/boundary validation, migration smoke test and correlation IDs (#10198)
Adds test harnesses for tenant isolation, TypeORM/MikroORM parity, persistence invariants and idempotency; startup validation for DB config; Nx plugin/core boundary rules; a fresh-database migration smoke test; and a correlation id carried from HTTP requests into docs queue jobs and logs. Testing (packages/core/src/lib/core/testing): - tenant-isolation: an in-memory tenant-aware repository, fixtures and strict assertions (a non-empty page, every row in the caller's tenant). Applied to EmployeeService and OrganizationProjectService. - orm-conformance and persistence-invariants: suites that run the same checks under TypeORM and MikroORM (run-both-orms.sh uses yarn nx). - idempotency: assertion helpers and specs for token cleanup (positive control), employee notifications and the Zapier timer webhook. - database/migration-smoke.spec.ts: runs the full migration chain on a fresh better-sqlite3 database. It is excluded from the default core jest run; run it with `nx run core:test-migration-smoke`. Idempotency: - EmployeeNotificationService.create() takes an opt-in `absorbRedelivery`. With it set, an identical unread, unarchived notification under 60 s old (same receiver, entity, type, sender, title and message) is returned instead of inserted, and a missing key or a failed lookup inserts as usual. The event handler does not enable it (the in-process EventBus never redelivers), so every caller inserts one row per event as before. - ZapierWebhookService skips resending a webhook that already succeeded for the same subscription, action and time log within 5 minutes. The key is reserved while in flight, failed deliveries stay retryable, pruning stops at the first unexpired entry, and the cache is capped at 10,000 entries. Config (packages/config): - An unknown DB_TYPE fails fast with the list of supported values; an empty DB_TYPE still means better-sqlite3; mongodb throws an Error. - Pool and timeout variables are parsed with Number.parseInt semantics, only for postgres and mysql. They throw only where tarn already refused to start and warn otherwise. SQLite ignores them and still prints the startup values. Observability: - RequestContextMiddleware accepts an inbound x-correlation-id of 1-128 visible ASCII characters (otherwise it generates a UUIDv4) and echoes it on the response. CORS allows and exposes X-Correlation-Id. RequestContext.currentCorrelationId() is added. - The docs queue carries the correlation id through job payloads (including bulk reindex) into pipeline outcome, error, dead-letter and enqueue-failure logs. Boundaries and build: - Every project gets a type tag. The ESLint depConstraints stop type:core depending on plugins, and plugins may depend only on core, shared libs and the known extension points (ai-chat, job-proposal, integration-ai, job-*-ui). - core declares @gauzy/scheduler (package.json and implicitDependencies); the webapp Dockerfile copies scheduler's package.json. - The integration-zapier jest config can import @gauzy/core (transformIgnorePatterns, allowJs, isolatedModules). - Fixes the stale @nrwl/nx eslint-disable id in the e2e roles-permissions steps and the broken @gauzy/core mock in the docs document-scope spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
da19222a45 |
Feat/polish timesheet details (#10221)
* style(timesheet): size the page title and breadcrumb at 12px * fix(timesheet): let each tab's card fill the page down to the footer * style(timesheet): tighten the filter row on the Calendar, Weekly and Daily tabs * style(timesheet): set all text on the Calendar tab to 12px * fix(timesheet): stop hour rows showing through the calendar's sticky day header * feat(timesheet): open the Daily selection actions in the filter row * style(timesheet): set Daily row text to 12px * refactor(timesheet): move the View page's inline styles into classes and drop a debug log * fix(timesheet): align View page rows with their header and mark the selected log * feat(time-log): lay out the Edit Time Log form on one shared grid * fix(time-log): return to the View Time Log popup when the edit is cancelled * fix(time-log): fit the View Time Log pane to its card so the backdrop closes it * feat(time-log): redesign the View Time Logs popover with a header, aligned rows and a footer * fix(timesheet): cap the Weekly tab's text at 12px like the Calendar tab * feat(timesheet): redesign the screenshot slot popup with a header, summary strip and aligned time logs * fix(timesheet): give the slot popup's screenshots an employee id so the viewer opens on the clicked one * feat(timesheet): move the slot popup's summary into its header and put apps beside their title * style(timesheet): fit the slot popup without a scrollbar and match its thumbnails and bar to the Recent Activities card * fix(gallery): open the screenshot viewer without focusing its first button * feat(gallery): redesign the screenshot viewer with a top bar, round nav buttons and a filmstrip * style(gallery): darken and blur the viewer backdrop and dim missing-screenshot placeholders * feat(gallery): sort the viewer by time, step with arrow keys and keep the active thumbnail in view * feat(i18n): add screenshot labels that name each screenshot by its time or place in the set * fix(timesheet): give the slot popup's screenshots alt text and name their info button * fix(timesheet): let the Time Logs popover's time range wrap on narrow screens * fix(gallery): name the viewer's info button, image and thumbnails and mark the selected thumbnail * fix(gallery): scroll the filmstrip without animation when reduced motion is on |
||
|
|
abd838e202 |
fix(desktop): mirror ui-core style tokens in desktop apps' shadowing SCSS copies
The desktop-side Angular apps (desktop, desktop-timer, server, server-api,
agent) put apps/<app>/src/assets/styles ahead of
packages/ui-core/static/styles in stylePreprocessorOptions.includePaths.
Their own var.scss and gauzy/_gauzy-overrides.scss therefore shadow the
ui-core files for every bare `@use 'var'` / `@use 'gauzy/_gauzy-overrides'`
in shared ui-core stylesheets, and tokens added only to ui-core are
undefined in these builds:
- $tabset-dsk-tab-padding-block / -inline (#10160), read by
includes/_tabset.scss and gauzy/_tabset-actions.scss
- $control-hairline / $control-padding-x (#9857), read by
time-tracker.component.scss since #10207
This has failed build-desktop on develop since
|
||
|
|
dd87b6f71c |
fix(export-import): keep import archives private, cover plugin entities and stop re-imports clobbering secrets
The uploaded import archive is no longer deleted after an import, because the Import page offers it for download again. It is instead stored under an unguessable dotfile name that serve-static refuses to publish, the public fullUrl derived from it is gone (ImportHistorySubscriber removed), and the file is served only through a new GET /import/history/:id/download route behind the existing tenant and permission guards, with an explicit tenant condition that refuses requests without a tenant. The Angular Import page downloads through that route. The export redaction guard now walks every plugin entity in the workspace as well as the core entities, loading them against the entity layer rather than the full core barrel. It immediately caught three plugin columns, all classified with reasons. A PLATFORM inbound address embeds its token, so the address column is now masked around the token via the new maskEmbeddedSecret helper. Re-importing an archive into the tenant it came from no longer writes redaction placeholders over live values on the update path, which would have broken public invoice links and nulled password digests. Export hardening: masking no longer skips the literal strings 'null' and 'undefined'; SMTP credentials use a fixed opaque mask; the tenant API secret digest and user email token are blanked; on/off flag spellings round-trip; a failed export job setup removes its scratch directory; junction rows that carry anything but foreign keys are refused. The IsSecret half of the tenant-setting check is now tested on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
327dc23cf3 |
fix: remove comments from PWA manifests so they parse as JSON (#10211)
apps/server/src/manifest.json and apps/server-api/src/manifest.json each carried a 43-line commented-out "icons" block. A web app manifest is parsed as strict JSON, where // comments are not valid, so both files fail to parse. They are copied into the build output by the src/**/* asset glob, so the invalid files ship. apps/gauzy/src/manifest.json is the same manifest with that block already removed, and it is the one actually referenced via <link rel="manifest">. This brings the other two in line with it. The commented-out icons were not enabled because the paths they point at (assets/icons/icon-72x72.png and the rest) do not exist in either app; uncommenting them would produce a manifest referencing missing files. All seven key/value pairs are unchanged. Files now use tabs per .editorconfig and .prettierrc, and end with a newline. |
||
|
|
86c0be350b |
fix: repair web manifest and license links (#10202)
* fix: repair web manifest and license links * docs: correct LICENSES link label |
||
|
|
8c5c24396e |
Merge pull request #10182 from ever-co/fix/polish-all-table-pages
Fix/polish all table pages |
||
|
|
c53db3d7c0 |
Fix/polish organizations pages 2 (#10160)
* style(tables): add the shared list-page block to the table hub
Two mixins, so the eighteen data tables under /pages/organization stop being
eighteen places for the same contract to drift.
`list-page($actions)` bundles what Payments, Invoices, Pipelines, Tasks,
Employees and Job Employees each spell out by hand: density tokens, the table
surface, row hover/selection, the tag-chip stack, the status pills, the pager's
`<nav><ul><li>` and the page-header action row. Those pages interleave
page-specific rules between the blocks, which is why they stay written out; these
have no such exceptions to interleave.
`$actions` defaults to `nb-card-header` rather than `.gauzy-button-container`,
and that is the point: several of these pages put a second row of buttons beside
the title — Inventory's Merchants / Warehouses / Product categories / Product
types, Equipment's "Equipment Sharing" — outside the toolbar container entirely.
Naming only the container left those at full size next to a compacted "+ Add".
Every token the row needs is a custom property, so one declaration on the header
reaches all of them, `ngx-gauzy-button-action`'s own template included.
`thumbnail-row-padding()` restores the app-wide cell padding on a table that
carries a picture column. `density-tokens()` runs at 0.1875rem/0.4375rem, which
is right for a row whose tallest thing is a line of text; a thumbnail row is not
that row, so the tighter box buys no height there and only presses the picture
against the rules above and below it. Set on `angular2-smart-table`, since the
density rules in `_overrides.scss` read these as custom properties inherited by
the `td` — no specificity needed against a `:root`-prefixed block.
The pictures themselves are deliberately left alone: shrinking them to the row's
scale costs the photo its information, and `ga-no-image` clips its "Image not
available" caption mid-word, so a missing image reads as a broken one.
* style(equipment): bring the equipment table onto the list-page scale
Adds the shared block, so this table reads as the same surface as Job Employees
and the rest of the list pages rather than as one drawn a size larger.
The action row takes the block's `nb-card-header` default, which this page needs
twice over: its toolbar sits in a bare `<div class="align-self-end">` with no
container class, and the "Equipment Sharing" button sits beside the title,
outside the toolbar altogether.
Three cells hard-code a box that the density tokens cannot reach, all scoped to
the table rather than the page because `ga-card-grid` renders the same components
for the card layout, where the full-size treatment belongs:
* Image — `ImageRowComponent` keeps its 74x60 photo; only the cell padding
around it goes back to the pre-density default.
* Equipment Name — `ga-picture-name-tags` renders `<ngx-avatar
class="report-table">`, which pins a 20px image, a 12px name and an 8px gap
inside a padded pill. Sized off the shared `gauzy-people-*` tokens, the same
treatment Employees gives its own Full Name cell.
* Auto Approve — a bare tick or cross, but `nb-icon` defaults to 1.5rem, half a
rem taller than the row's whole line box, so a column carrying nothing but a
glyph was setting the height of every row. Re-tokenised through Nebular's own
`icon-*` keys, so there is no specificity to fight.
Also removes `:host nb-card-body { height: calc($card-height - 0.5rem)
!important }`. `gauzy/_gauzy-cards` already supplies that same chrome estimate as
a FLEX BASIS, which `flex: 1 1 auto` then corrects against the card's real
height; restating it at page level with `!important` turned a starting point into
a floor and a ceiling.
* style(equipment-sharing): bring the sharing table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the status pills, the pager and the button scale.
The Status column already routes through `ga-status-badge` (see `statusMapper()`
in the component), which is exactly what the block's tint rules address — the
saturated fill with white text becomes the accent-on-tint treatment the rest of
the app uses, and lands the accessible colours with it.
The action row takes the `nb-card-header` default, which also picks up the
"Equipment Sharing Policy" button sitting beside the title.
* style(equipment-sharing-policy): bring the policy table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale — so this
table reads as the same surface as its neighbours.
* style(tags): bring the tags table onto the list-page scale
Adds the shared block. The action row takes the `nb-card-header` default, which
covers this page's `.gauzy-action` toolbar — it lays its header out itself,
beside the tag-type rail, and never adopted the shared container class.
The pager was the reason for the one template change: `<ngx-pagination>` was
rendered bare, with no `.pagination-container` around it, so it was one of three
pagers in the section the styling could not reach. Wrapping it matches every
other list page and the Job Employees reference. No bindings, handlers or
conditions touched.
The wrapper also has to opt out of growing: `.table-scroll-container` beside it
takes `flex-grow: 10` from this sheet, which would otherwise leave the pager
fighting it for the remainder of the card body.
* style(approval-policy): bring the approval-policy table onto the list-page scale
Adds the shared block: density tokens, the table surface, row hover/selection,
the tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale.
* style(departments): bring the departments table onto the list-page scale
Adds the shared block. The Members column renders `EmployeeWithLinksComponent`,
which draws through `ngx-people-list` — already built on the `gauzy-people-*`
tokens, so the density tokens this block puts on `:host` reach it across the
component boundary and the people cells come onto the row's scale for free.
* style(teams): bring the teams table onto the list-page scale
Adds the shared block. Members and Managers both render
`EmployeeWithLinksComponent` and the Tags column renders `TagsOnlyComponent`, all
three of which already read the shared tokens — so the density block on `:host`
carries them onto the row's scale across the component boundary.
* fix(projects): pass the content column's height through the routing shell
This component is the /pages/organization/projects shell: its whole template is a
`<router-outlet>`, and the list, create and edit pages render into it.
`styles/_overrides.scss` hands the routed page the viewport-bounded height the
content column already has — but with `nb-layout-column > router-outlet + *`, a
DIRECT-child selector, so on a nested route it lands on THIS component and stops.
Below it the page had no definite height for `nb-card { height: 100% }` to
resolve against, the card collapsed to its content, and the table inside came out
short of the card, taking its scrollbar with it.
NOT fixed with `:host { display: flex }`, which is the obvious way to hand a flex
height on and cannot win: `pages.component.scss` carries
`:host ::ng-deep router-outlet + * { display: block }`, which shims to
`[_nghost-pages] router-outlet + *` — (0,1,1) — and reaches every routed
component in the app. A component's own `:host` is `[_nghost-x]`, (0,1,0), and
loses. This host stays `display: block` whatever this file says.
What the host DOES get from that same `_overrides` rule is `flex: 1 1 auto` — a
different property, so nothing overrides it — which makes its own height
definite. A percentage resolves against that perfectly well and, unlike a flex
child, does not care what the parent's `display` ended up being. So the routed
page simply takes 100% of the shell.
`::ng-deep` is required and is not a leak: the routed component is a DOM sibling
of `<router-outlet>` but belongs to a different component, so it carries none of
this template's `_ngcontent` attribute and a plain `router-outlet + *` would shim
to a selector matching nothing. Leading with `:host` keeps the rule inside this
shell, and setting no `display` keeps it clear of the rule above.
* style(projects): bring the projects table onto the list-page scale
Adds the shared block, with the action row on the `nb-card-header` default — this
page's toolbar sits in a bare `<div class="d-flex justify-content-end">` with no
container class of its own.
Three cells hard-code a box the tokens cannot reach, all scoped to the table
because `ga-card-grid` renders the same components for the card layout:
* Project — `gauzy-project-organization` draws a 28px logo beside a 14px/17px
name and an 11px/13px caption, the single biggest reason a row here stood
taller than the same row on Employees. Sized off the shared `gauzy-people-*`
tokens so the project chip matches the people cells beside it.
* Contact — `ngx-contact-links` draws an 18px avatar and 12px/15px text inside
a pill padded 5px/14px. Same scale-down; the pill keeps its tint so the cell
still reads as a link.
* Public/Private — an `nb-toggle`, which Nebular sizes entirely from theme
tokens: a 50x30 switch with a 28px knob and a label on a 1.5rem line, i.e.
twice the row's whole line box. Re-tokenised to 20x12, which keeps Nebular's
own 1.667 proportion so it reads as the same control drawn smaller — the same
numbers Employees gives its Screen Capture column.
Also records why the card fits: this page is reached through a routing shell, so
the height chain is repaired in the shell's own stylesheet, which is the only
place it can be.
* fix(inventory): pass the content column's height through the routing shell
This component is the /pages/organization/inventory shell: its whole template is
one wrapper around a `<router-outlet>`, and the items table, the item form and
the item view render into it. It carried no stylesheet at all.
`styles/_overrides.scss` hands the routed page the viewport-bounded height the
content column already has — but with `nb-layout-column > router-outlet + *`, a
DIRECT-child selector, so on a nested route it lands on THIS component and stops.
Below it the page had no definite height for `nb-card { height: 100% }` to
resolve against, the card collapsed to its content, and the table inside came out
short of the card, taking its scrollbar with it.
NOT fixed with `:host { display: flex }`, which is the obvious way to hand a flex
height on and cannot win: `pages.component.scss` carries
`:host ::ng-deep router-outlet + * { display: block }`, which shims to
`[_nghost-pages] router-outlet + *` — (0,1,1) — and reaches every routed
component in the app. A component's own `:host` is `[_nghost-x]`, (0,1,0), and
loses. This host stays `display: block` whatever this file says, and a
`flex: 1 1 auto` on the wrapper would have nothing to grow inside.
What the host DOES get from that same `_overrides` rule is `flex: 1 1 auto` — a
different property, so nothing overrides it — which makes its own height
definite. So the wrapper takes 100% of it rather than trying to be a flex child
of a box that is not a flex container, and the routed page grows inside the
wrapper, which IS one.
`::ng-deep` is required and is not a leak: the routed component is a DOM sibling
of `<router-outlet>` but belongs to a different component, so it carries none of
this template's `_ngcontent` attribute and a plain `router-outlet + *` would shim
to a selector matching nothing. Leading with `:host` keeps the rule inside this
shell, and setting no `display` keeps it clear of the rule above.
* style(inventory): bring the shared inventory-table sheet onto the list-page scale
This file is taken by five tables — product types, product categories,
merchants, warehouses and the inventory items table — so the shared block lands
once for all of them: density tokens, the table surface, row hover/selection, the
tag-chip stack, the pager's `<nav><ul><li>` and the toolbar scale. They are the
same card with the same `.table-scroll-container`, `.pagination-container` and
`.gauzy-button-container` five times over.
Scoped to the table on top of that:
* The Image / Logo / Icon column — `ImageRowComponent` (74x60),
`ItemImgTagsComponent` (70x64) and `IconRowComponent` (a 35px disc) keep
their sizes; only the cell padding around them goes back to the pre-density
default, since a picture sets the row height whatever the text does.
* Name and description — both renderers hard-code a 14px/17px name over a
12px/15px caption, so this one column read a step louder than every other
column in the row. Pointed at the table's own sizes, which is a match rather
than a reduction. The 4px between the two lines is left alone.
* Active / Inactive — `EnabledStatusComponent` emits a bare
`<div class="badge badge-success">`, so the only thing painting it was
bootstrap.css: a saturated fill with white text, and the inherited
`color-success-default` measures 2.9:1 on white. Its counterpart was a literal
`#ccc` with whatever text colour it inherited — the one chip in these tables
that ignored the theme. Both now take the accent-on-tint treatment
`ga-status-badge` gets everywhere else.
* fix(inventory): let the items card fill the content column
Removes `:host nb-card-body { height: calc($card-height - 0.5rem) !important }`.
`gauzy/_gauzy-cards` already gives the body that same chrome estimate, but as a
FLEX BASIS — `flex: 1 1 auto` then grows or shrinks it to whatever the card
actually has, so the estimate being a guess never mattered. Restating it at page
level with `!important` turned a starting point into a floor and a ceiling.
/pages/tasks/dashboard, which fits, pins that height only on its `.project`
variant; with none selected its body is a plain flex child, and that is the whole
difference between the two pages.
With the line gone this page's compiled card and body rules are identical to that
reference, byte for byte.
The other half of the fit is not in this file: the height chain has to reach this
page at all before `nb-card { height: 100% }` can resolve, and that is repaired in
the routing shell.
* style(inventory): bring the warehouse product tables onto the list-page scale
The warehouse's own product table and the product picker it opens are the same
surface as the page behind them, so both take the shared block, the thumbnail
cell padding, and a `.pagination-container` around their pagers.
Both rendered `<ngx-pagination>` bare, which is why their `<nav><ul><li>` was out
of reach of the styling; wrapping it matches every other list page. No bindings,
handlers or conditions touched. Both wrappers also opt out of growing, since
`.table-scroll-container` beside them takes `flex-grow: 10`.
The action rows differ and are named rather than left at the `nb-card-header`
default: the products table has no card header at all — it IS a card body — and
puts its toolbar in a bare `<div class="d-flex justify-content-end">`, so the
button component itself is the one stable handle, and the tokens are custom
properties that inherit into its template from there. The picker has no action
row at all, so it passes `null`.
The picker's select cell keeps its literal `height: 79px`: `display: flex` takes
that `td` out of the row's own height resolution, so it has to state a height or
the checkbox stops lining up with the data beside it, and 79px is the 74x60
thumbnail in the next column — which this change leaves at 74x60.
* style(inventory): bring the variant tables onto the list-page scale
The variant table in the item editor and the variants table on the item view are
the same surface as the inventory list they hang off, so both take the shared
block. Neither has a pager, but the block is the whole contract and splitting it
per page is how the list pages drifted apart in the first place.
Scoped to the table on both: the Image column is `ImageRowComponent`, which keeps
its 74x60 photo and gets back the pre-density cell padding around it; the Enabled
column is `EnabledStatusComponent`, whose `.badge-success` was painted only by
bootstrap.css — a saturated fill with white text at 2.9:1 — and whose disabled
counterpart was a literal `#ccc`.
The editor's table names `.gauzy-button-container` for its action row; the item
view has no action row and passes `null`.
* style(tabset): draw both tab families from one definition
Nebular ships two components for one control — `nb-tabset` switches a panel,
`nb-route-tabset` switches a route — and every sheet in this repo that dressed a
tab strip had named only the route one. So the two were drawn differently
wherever both appear in the same app: the route strips (organization edit,
employee edit) took a 16px box and stacked their icons above the label below
`dsk`, while every `nb-tabset` (Employment types, Time off, Candidates, Jobs,
Inventory) kept the theme's own box and a 0.5rem inline icon gap.
The body of that rule becomes `tab-strip($strip)` and is included for both, so
they cannot drift again. The strip class is a parameter rather than a bare `ul`,
which would also catch a list inside a tab's CONTENT and strip its indent.
Also restores the keyboard focus ring the `outline: none` on `.tab-link` took
with it. That line is there to drop the ring a mouse click leaves behind, and it
removed the keyboard's too — on the one control a keyboard user moves through
with arrow keys. `:focus-visible` gives it back to both families without
bringing the mouse ring back.
* style(tabset): centre nb-tabset labels and stop shouting route tabs
The same oversight as the sheet before this one, in the two places that dress a
tab strip from the theme side.
`nb-tabset-overrides()` centred a tab link's contents for the route family only,
so an `nb-tabset` link took Nebular's own `display: flex` and nothing else and
its label sat wherever the box put it, while a route tab's was centred in the
same box. The icon-gap reset was route-only for the same reason: Nebular gives
the label a 0.5rem inline margin that would be added to the 10px gap
`includes/_tabset.scss` sets on the icon. Both rules now name both families.
And the reverse case in the themes: Nebular ships UPPERCASE for BOTH families
(`styles/themes/_mapping.scss`) and only the `nb-tabset` half of it was ever
overridden here, which is why an `nb-route-tabset` read MAIN / LOCATION /
SETTINGS while every `nb-tabset` in the app read its tabs in sentence case.
`route-tabset-tab-text-transform: capitalize` alongside the existing key, in
both registered themes — the same fix, and the same shout, the ghost-button line
below it describes.
* style(tabset): keep the action row on the strip's box below dsk
The action row that sits ON a tab strip is boxed to match the strip, and it
matched only at the default breakpoint: below `dsk` the strip takes the 16px box
from `styles/includes/_tabset.scss` and this row kept the theme's line height,
so it was no longer the strip's own row there.
Both families now take that 16px box — that sheet stopped naming only the route
one — so this row follows it at the same breakpoint. The inline padding stays
zeroed, as it is at the default size: the row is positioned by the strip's own
end, not padded away from it.
* style(table-hub): add the list-page contract for hand-built card tables
`list-page()` anchors every selector that matters on the `angular2-smart-table`
element, so it never reached the five pages under `/pages/organization` that
build their table BY HAND — Vendors, Positions, Employee levels and Documents
share one stylesheet, Employment types has its own. What that looked like:
full-size toolbar buttons beside the compacted ones on Departments and Teams,
rows better than twice the height, and a stack of separately floating cards
where the rest of the section has one bounded surface with rows on it.
`card-list-page()` takes the three pieces of `list-page()` that are NOT anchored
on the smart table — the density tokens, the toolbar scale, the table surface —
and adds the one thing these rows need that a `<tr>` never did: they arrive as
`nb-card > nb-card-body.custom-table`, carrying a card's background, radius,
ring and bottom margin, all of which is flattened onto the surface and replaced
with one inset hairline per row (a single-column list has neither aligned
columns nor anything else to give it structure).
Hover and selection come with it, for all five pages rather than the four that
share the Vendors sheet. `expense-categories` gives both states the same
`rgba(50, 50, 50, 0.03)`, so hovering looked exactly like selecting and 3% of
black over a near-black card is nothing at all — no visible selection on the
four dark themes. Selection also arrived as a 6px border that exists only when
selected, so the row jumped sideways on click. Now: the neutral tints, primary
on the leading edge, and a transparent edge of the same width at rest.
Two constraints are load-bearing and noted at their rules. The row paint is
deliberately `:host .custom-table`, the same specificity `expense-categories`
uses, because the hover and selection rules sit one pseudo-class above it —
anything stronger would out-rank them and paint every hovered row transparent;
that also means the mixin MUST be included after the `@forward`. And the surface
takes `min-height: 100%` rather than `flex: 1 1 auto`, since two of the five put
this container in a card BODY that is not a flex column.
The row gutter is a parameter: the default suits a row holding one value across
its width, while a row whose markup is a Bootstrap `.row` needs at least the
15px that grid bleeds past its container with negative margins.
* style(vendors): move the column titles onto the table surface
Four pages `@forward` this sheet — Vendors, Positions, Employee levels and
Documents — and all four build their table as a stack of `nb-card` rows, so they
take `card-list-page()`. The gutter is passed as 1.125rem: a row here is a
Bootstrap `.row`, which bleeds 15px past its container through negative margins,
so at the default 0.4375rem the first cell would hang 8px off the left edge of
the surface. The local hover/selection block goes with it — it is now in the
shared mixin, in logical properties, and reaches Employment types too.
Vendors is the only one of the four with column titles, and they were a filled
card-2 band in the CARD HEADER: a second surface floating 10px above the rows
with its own rounded corners, and one that could not line up with the values it
titles, since it sat in a container with 1rem of padding while the rows sat in
one with 18px. The `pl-3` on the first title was there to paper over exactly
that difference.
They are now the first child of the scroll container, boxed exactly like a row —
same padding, same 3px rail reserved on the leading edge for the selection
border — so the titles sit over their columns by construction rather than by
nudging, and `pl-3` comes off. The treatment is the one `list-page()` gives a
smart table's `thead`: no fill, the muted heading colour, one hairline
underneath, and `position: sticky` so it stays while the rows scroll under it —
which is why the fill has to be the opaque surface colour rather than none.
The `@if` on the layout enum moves with the block; it guarded the titles in the
header and still guards them in their new home.
* style(employment-types): bring the hand-built table onto the list-page scale
The fifth of the hand-built tables, and the one that does not share the Vendors
sheet — so it had neither the list-page scale nor any row hover or selection at
all, only what `expense-categories` gives it. Rows were separately floating
cards on the tab body, and the toolbar sat at full size beside the compacted
buttons on Departments and Teams.
The action row is the one thing this page positions itself: it does not sit on
the title line like the rest of the family, it sits ON the tab strip
(`tabset-action-row`), so the toolbar scale is asked for by name —
`.gauzy-button-container` — rather than taken on `nb-card-header`, where this
page has no buttons at all. The `+ Add` button in the template takes `.action`
with the rest of the section, so it is picked up by that scale.
* style(organization): compact the add button on the remaining card-list pages
Positions, Employee levels and Documents take the Vendors sheet and therefore
the toolbar scale that came with it, but their `+ Add` buttons were never
classed `.action`, so the scale had nothing to select and they stayed at full
size beside the compacted ones on Departments and Teams. One class each; no
bindings, permissions or handlers touched.
Documents also had its file timestamp set at `text-caption-font-size`, which is
a step ABOVE the row's own size rather than under it — the secondary value was
rendering larger than the file name it hangs off. It now takes
`--gauzy-table-font-size`, the size the shared card-list block sets the row at.
* fix(company-logo): slug the brand class and fall back to a generic glyph
The company name was interpolated into the class list as it stood
(`'fab fa-' + rowData?.name | lowercase`), which is only a class at all for a
single-word name. "Ever Technologies LTD" produced THREE classes — `fa-ever`,
`technologies` and `ltd` — so any company whose name happens to contain a word
the page styles (`selected`, `primary`, `action`, `row`) was styled by it. The
name is now slugged to one class whatever it contains: lower case, ASCII, hyphen
separated, which is also the form Font Awesome's own names take, so "Pay Pal"
finds `fa-pay-pal` where before it looked for `fa-pay`.
The bigger half is that Font Awesome only ships a logo for companies that ARE
brands, and this table lists a tenant's own vendors — so for most rows `fa-<name>`
matched nothing and the cell drew a blank. FA still gives every icon element a
fixed 1.25em box, so the column paid for an icon it never showed, and the rows
that did match a brand were the only ones with anything on their leading edge.
FA 7 draws its glyph from a custom property (`content: var(--fa)`, set by the
`fa-<name>` class), and custom properties inherit — so a value declared on the
HOST is what the `<i>` gets when no `fa-<name>` class set one, and is overridden
by the class whenever one did. A generic building for a company Font Awesome has
never heard of, with no branch in the template and no list of which names it
knows. The font stack gains the free face behind the brands file, since that is
where the fallback glyph lives, and weight 900 to reach its solid face — harmless
to the brands file, which ships one weight and is matched whatever is asked of
it.
Sizing follows the row rather than a fixed 18px/21px, and the 0.75rem of padding
on all four sides — the better part of a centimetre around a one-em glyph, and
for every logo-less row the only thing there at all — becomes the one gap an icon
column needs, to the name beside it.
* style(inventory): put the section buttons on the title's line
Merchants / Warehouses / Product categories / Product types sat in a nested
`.card-header-title` block of their own, which the outer row's
`align-items: center` then centred against the TWO-line title block — the
heading plus the breadcrumb trail `ngx-header-title` parks under it — dropping
them ~12px below `+ Add`. The heading and the buttons are now direct siblings of
ONE title row, the shape Employees and Equipment already use, so they share the
title's line and therefore `+ Add`'s.
They stay wrapped in a single block so the row's `justify-content:
space-between` (`styles/_overrides.scss`) pushes the four of them to the row's
end as a group, rather than spreading them evenly across it as four separate
flex items would be.
That block is flex, not a plain box: as inline-blocks the buttons sit on a
baseline and the descender space under it makes the box a few pixels taller than
they are — the same offset this change removes, in miniature. The two
declarations that settle the vertical axis are `align-self: flex-start` and
`min-height: gauzy-page-title-row-height`, the pair `_overrides.scss` already
puts on `.ga-page-header-actions` beside it: centring alone is not enough while
the two blocks are centred in different boxes, so both are anchored to the row's
top edge in the same box and share one centre by construction.
`flex-wrap` folds the group onto a second line on a narrow viewport rather than
squeezing the labels, and `min-width: 0` lets it shrink at all.
* style(tags): size the columns, the name chip and the filter rail
The four column widths are a RATIO the library hands to the `<th>`s, so they
have to add up to the table: 20/20/70/10 came to 120%, which is why Description
alone took better than half the row and the other three were squeezed into what
was left. They now total 100. The Name column's `class: 'text-center'` goes —
`class` is not one of the library's settings and was dropped on the floor;
`classHeader` and `classContent` are the real hooks, and the Counter and Tag
type columns use them.
Counter is right-aligned with tabular figures: a count belongs on its units
digit, where "3990" and "1890000" no longer start in the same place, and Inter's
proportional default figures otherwise wander a pixel or two per row. Its
heading follows, via `justify-content` rather than `text-align` — `a.sort` is
laid out `row-reverse`, so the end of its main axis is the left edge. Six-figure
usage counts are common here and are now grouped by the browser locale. Tag type
and Description fall back to an em dash and Tag type takes the table's muted
colour, so the row's three values stop competing at equal weight and a blank
column reads as deliberately empty rather than as a table that failed to load.
The name chip was `width: 100%` — ten saturated full-bleed bars of identical
length down the left edge, whether the label was "VIP" or "dependencies". Sized
to its text the colour is an accent again and the names line up under the "Name"
heading. `nb-badge` sets `position: absolute` on its own host, which is what
forced the wrapper to state a height for a chip contributing none; one
`position: relative` puts it back in flow so the wrapper can align it like any
other child.
The rail: chips take the control step of the radius scale rather than a 20px
stadium — the one shape that matches nothing around it — and so do the search
field and the badges. A chip may now wrap, since "Organization Department" is a
type Gauzy ships and a chip that could neither wrap nor shrink was wider than
the 230px rail, cut off by the card edge and dragging a horizontal scrollbar in
with it. The rail's type drops to 0.75rem throughout, the size of the table
headings beside it, so a filter stops out-typing the rows it filters; the
section label was the largest type on this half of the page and sat over the
smallest controls on it.
The search input opts out of the app-wide `min-height` floor that
`text-inputs-overrides` puts on every bare `input`: it was drawn ~10px taller
than the pill around it, invisible only because the field is transparent, but
the click target and the caret were both outside the box.
And the chips are reachable: they are the page's only filter control and plain
`nb-list-item`s, so they get `role`, `tabindex`, Enter/Space, `aria-pressed` and
a focus ring. The selected one gains an accent inset ring — the tint alone is
one step off hover and the resting chip already carries a neutral hairline, so
which of a dozen chips is filtering the table has to be findable from across the
page.
Rows get their air back locally: `density-tokens()` sizes a row for a line of
text and this table is four short single-line values and a chip, so nothing ever
pushed a row past that floor. The cells take the app-wide default instead, the
same value `thumbnail-row-padding()` hands the picture tables, with the heading
row raised to match. Scoped to this page — widening the shared token would
relayout two dozen tables nobody asked about.
* style(teams): size the columns and give the avatar rows their own rhythm
Set explicit column widths (20/30/30/20) so `table-layout: auto` stops sizing
the two people columns off their widest cell and holding the space as empty,
and drop the inert `class: align-row` the library never reads.
Step the row padding to 0.4375rem: an avatar fills its line box edge to edge
where a glyph fills about two thirds of it, so the shared text floor left the
discs 3px from the rules above and below them.
Zero the `ga-only-tags` block gap in the Tags column, which has chips and no
text above them for that gap to separate.
* fix(ui): address review findings on the tags, vendors and tabset polish
Keep the counter heading on its column under RTL. `justify-content` resolves
against the flex container's own direction, so `flex-start` on the `row-reverse`
sort link swung left while the cells stayed physically right. Overridden with
`:host-context([dir=rtl])` rather than `nb-rtl()`, which expands to `[dir=rtl] &`
and would ask for a `:host` nested inside another element.
Give the tab strip's `dsk` box a single source of truth. `px2rem(16px)` was
hand-copied into `_tabset.scss` and `_tabset-actions.scss`, where the action row
has to match the block value to stay vertically centred on the tab labels — the
exact drift the mixin's "WHY THIS EXISTS" note was written about. Both now read
`$tabset-dsk-tab-padding-*` from `var.scss`, which both sheets already use.
Close the vendors table surface at the top. `surface()` draws its outline with
`box-shadow: inset`, which is part of the element's background layer, so the
opaque sticky head painted straight over the top line and the leading edge. A
real border is geometry rather than paint: it sizes the content box the head is
laid out in, fixing top, leading edge and corners at once and mirroring under
RTL on its own.
Correct the `class` rationale. `class` IS declared on the library's `IColumn`,
which is why a dozen tables pass it without a type error, but `Column` never
reads it and it reaches no element — the effect claim was right, the reason was
not.
Correct the documents timestamp fallback and its reasoning. The row is
0.8125rem, so the previous `text-caption-font-size` was already smaller, not
larger; the fallback now sits below the row size instead of mirroring it.
Model the tag-type rail as a radiogroup. The options are mutually exclusive, so
`role="button"` + `aria-pressed` announced them as independent toggles and never
said the previous choice had been cleared. Now `role="radio"` + `aria-checked`
in a `radiogroup`, with one tab stop and arrow/Home/End navigation.
Reword the comment wording CSpell rejects.
* fixed the spell error
* fix(ui): clear the SonarCloud findings on the polish branch
Reliability, which is what failed the quality gate:
The Font Awesome stack in the company-logo cell ended on two names that
are not web-safe, so it had no last resort at all; it ends on a generic
now. And the slug trim's `/^-+|-+$/g` had two ambiguous quantifiers for
the engine to backtrack over — redundant ones, since the replace above
it has already collapsed every run of non-alphanumerics into a single
hyphen, so at most one can sit at either end. `/^-|-$/g` is the same
output with nothing to retry.
Maintainability:
`String(name ?? '')` took an `unknown`, so a non-primitive stringified
to `[object Object]` and slugged into a class of its own; it now takes
the value only when it is a string or a number.
Three sheets reopened a selector they had already written. Tags needed
the care: its second `:host` widens two density tokens that
`list-page()` sets on `:host`, so merging upward would have let the
shared value win — the include moves above the block instead, and the
tokens sit at the top of the rule rather than after the nested
selectors, which Sass deprecates. Vendors' stray `padding-block` moves
in beside the shorthand it overrides, and the projects shell's second
`:host` folds into its first.
* fix(ui): keep the tag filter rail reachable, and the vendor chips inside their cell
The rail is a radiogroup, so its tab stop is the checked radio — and a
refresh used to take that radio away. `getTags()` emptied
`filterOptions` down to "All" and `getTagTypes()` refilled it a request
later, so for the length of that request `selectedFilterValue` named a
type no rendered option carried: every radio went `tabindex="-1"`, and a
keyboard user whose option had just been re-rendered away had nothing
left to Tab back into.
Three things, because the window was only half of it. `getTagTypes()`
now builds the whole list and swaps it in one statement, so the rail
never renders a state the selection does not match, and `getTags()` no
longer touches it. The `@for` tracks `option.value` rather than the
option object — the old identity tracking rebuilt every radio on a list
of fresh literals, focused one included, which is what actually dropped
focus to the document. And the tab stop falls back to the first option
when nothing is checked, which is what the radiogroup pattern asks for
and covers the cases where the selection really has gone: an
organization switch, or a tag-type fetch that failed.
Elsewhere: a vendor tag longer than its two-column cell kept its chip
wider than the cell and ran it out through the side of the table, since
the chips may not wrap; they stop at the cell's edge with an ellipsis
now. And the specificity noted in the Teams tag-cell comment was (0,2,5)
for a selector that is (0,2,4) — the override still wins, but the count
was wrong for anyone extending the rule.
* fix(tags): drop a superseded load's response instead of applying it
The refresh generation gated whether the second load STARTED, which is
not the same question as whether its answer is still wanted. A
`getTagTypes()` that was current when it went out can be answered after
a newer one — two requests for the same resource settle in either order
— and on arrival it rebuilt the filter rail from the organization the
user had already left, then reconciled the selection against it.
Both loads now take the generation and check it again when their own
request comes home: a superseded response is dropped rather than
applied. `getTags()` gets the same treatment at its own network
boundary, since `allTags` and the pagination totals are what the
reconcile then reads, and leaving half the pipeline guarded would only
have moved the race.
Three details worth naming. `finally` runs on those early returns too,
so the reconcile and the spinner are guarded there rather than left to
fire for a pass that has been superseded — the spinner belongs to
whichever pass is current, and that pass always reaches its own
`finally`. A failure is still logged whatever its generation, because a
console line costs nothing and a swallowed error is hard to find later;
only the toast and the rail reset are held back. And the generation
parameter defaults to the current one, so a standalone call to either
method behaves exactly as it did.
|
||
|
|
33e3c09f30 |
Feat/hr dashboard redesign (#10125)
* feat(theme): add validated chart series tokens The employee statistics charts painted with Nebular's raw status colours, which are tuned for text rather than for large fills. Adds gauzy-chart-* to the shared token maps, so every registered theme carries its own steps for the four series, the negative state, the grid and the tick labels. Merged through $gauzy-shared-light/dark, the same path the action accents already take into all eight themes. Every value was snapped to a step that clears all six checks of the categorical palette validator against the surface the charts sit on: the OKLCH lightness band for the mode, the chroma floor, all-pairs CVD separation, the normal-vision floor and 3:1 contrast. Danger is reserved for the negative state and so cannot double as the fourth categorical hue; fuchsia is the step that clears blue under deuteranopia, where violet and indigo both fail outright. * refactor(dashboard): resolve chart palette from theme tokens Reads the gauzy-chart-* custom properties off a live element instead of Nebular's JS theme variables, so the charts follow the same steps the rest of the page paints with and pick up a theme switch for free. Falls back to the previous status colours when a token is absent or when there is no host element, so nothing regresses outside a browser. Also exposes the surface, border and primary ink the tooltips need to render as a card on the theme's own ground. * feat(dashboard): add shared employee chart options The three employee statistics charts each spelled out their own legend, tooltip, scales and mark geometry, which is how they drifted apart. Collects that chrome in one module: a legend of small dots, a tooltip painted as a card on the theme surface, a hairline grid on the value axis only, and thin marks with rounded ends separated by a gap in the surface colour rather than by an outline. Axis ticks are abbreviated so a money axis stays on one line without rotating its labels, and the chart's own animation honours prefers-reduced-motion, which no CSS media query can reach. The tooltip takes an optional value resolver because the number a mark plots is not always the number it should report. * chore(dashboard): export shared chart options from barrel * style(dashboard): restyle employee horizontal bar chart Adopts the shared chrome: grid on the value axis only, no axis borders or tick spurs, and thin rounded bars with a surface gap in place of the 2px border each bar used to carry. The container no longer pins itself to 20rem, a height that took no account of the legend above the plot or the tick band below it. It now fills the box the panel gives it. * fix(dashboard): migrate stacked bar chart to Chart.js v4 options The chart configured itself with v2 option names - legend, tooltips, elements.rectangle and labels.fontColor - which Chart.js 4 silently ignores. It therefore rendered with library defaults and never followed the active theme at all. Moves those onto plugins.* and the shared chrome, and formats tooltip values as currency the way the sibling charts do. The tooltip reports the unscaled figure rather than the plotted one: this chart normalises each month's segments so the stack sums to that month's income, so the plotted value is a share and printing it as money would state something untrue. Also drops dead code. selectedDate and proportion were declared as fields and never assigned, so three dataset label ternaries had always taken their else branch; proportion is now a local in the loop that computes it. * fix(dashboard): rework employee doughnut chart layout The slice labels were the full "Revenue: $12,450.00" strings, and a doughnut builds its legend from those, so four long entries wrapped across the top of the chart and squeezed the ring. The tooltip then echoed the same string back, overriding the currency formatting. Moves the legend to the right, where a circle in a wide box has room to spare, and caps its width so a long amount cannot push the ring into a corner. Slice labels are names only; the amount is appended by the legend itself and the tooltip formats its own. Types the chart configuration as 'doughnut'. Bare ChartConfiguration resolves to the union over every registered chart type, whose options are only those common to all of them, so arc-only settings such as cutout and hoverOffset are not known properties there. Removes the tooltip helper that only echoed the slice label, along with four now-unused locals and a commented-out span. * refactor(dashboard): make employee charts fill their host panel The component rendered its own chart-type switcher right-aligned above the canvas, which on the HR dashboard produced a second header-ish row directly beneath the panel's real header with nothing to anchor it to. selectedChart becomes an input, defaulted so the component still renders standalone, and the host owns the control. Also fixes the sizing. align-items: center on a flex column is the cross axis, so the chart shrank to its intrinsic width and floated in the middle of the panel instead of spanning it. * refactor(dashboard): rework HR dashboard component state Formats every money figure once, on load, instead of repeating a ternary plus two pipes per figure in the template. Fixes averageBonus, which the header has always had a row for but which was declared and never assigned, so the row could not render. It is the period total over the number of months the API returned. The page also rendered nothing until the debounced selectors resolved. loading now starts true and is released on the early return as well, so the first paint is a spinner rather than a false empty state. Adds the chart type the statistics panel draws, the initials shown when an employee has no avatar, and the bonus formula the Total Bonus tile puts on its tooltip - resolved here because a tooltip is an attribute binding and cannot host control flow. * feat(dashboard): restructure HR dashboard template The page was a two-column split whose left column was one flat stack of label/value rows, so the totals carried no more weight than their own components and there was nothing to scan. It now reads as identity, then the four totals, then what they are made of beside the same figures over time: an identity bar naming the employee the figures belong to, a KPI row of four tiles, and a Breakdown panel next to the Employee Statistics panel, whose header carries the chart-type switcher on the title's line. A tile carries a label and a number; the arithmetic behind it moved to its tooltip, since four tiles each printing two clamped lines of grey formula read as a wall rather than a summary. Breakdown rows are single line for the same reason. Clickable figures are real buttons rather than click handlers on divs, so they are reachable by keyboard and announced as actions, and each names itself with its label and value. Adds empty states for no employee selected, and decorative icons are hidden from assistive tech. * style(dashboard): rebuild HR dashboard styles Paints entirely with the shared gauzy-* tokens, so all eight themes and both canvases follow without a per-theme branch. Type weight is a three step scale, and only three. Nearly every element on the page was 600 - the employee name, both panel titles, the group headings, the KPI values, every figure - and when everything is semibold nothing reads as emphasis. Hierarchy comes from size and the muted or strong ink tokens instead, and weight is spent on the four headline numbers alone. The panel header's min-height is derived from the chart switcher it has to hold, so the Breakdown and Employee Statistics headers line up and their bodies start on the same line. The chart's height is set by the panel rather than by the canvas, so plot, legend and axis labels live in one measured area. Money figures use tabular numerals, breakdown chevrons sit out of flow so values align in a column whether or not a row is clickable, and the whole page respects prefers-reduced-motion. * feat(i18n): add HR dashboard breakdown label Names the panel that itemises what each headline total is made of. * test(e2e): retarget HR dashboard selectors The HR dashboard no longer renders ga-info-block, so the selectors that found its cards matched nothing and every card assertion and click in the suite would have failed. Points them at the KPI tiles and breakdown rows that replaced it, and excludes the Total Bonus tile, which is deliberately static because there is no BONUS history type to open. Records why the text filters are now unambiguous: each queried heading renders in exactly one card, because the formulas that quote other headings live on tooltips, which Playwright's hasText does not see. * test(e2e): update HR dashboard page helper notes Describes where the click handler now sits and why .first() resolves unambiguously, replacing notes that described the removed info-block markup and a DOM ordering the page no longer depends on. * docs(dashboard): correct chart switcher references in widgets Both files described the switcher as something ga-employee-charts puts above its own chart. That control now lives in the HR dashboard's Employee Statistics panel header, and the component only draws the rendering it is handed. * fix(dashboard): size employee charts from a positioned parent With `maintainAspectRatio` disabled, Chart.js measures the canvas against its offset parent. `.chart` was unpositioned in all three employee charts, so the plot sized itself against an ancestor further up the tree and came out small in the middle of the panel. * fix(dashboard): drop amounts from the doughnut legend The amounts were appended to each legend entry, producing four ragged "name + figure" rows whose numbers did not line up — a legend cannot lay out two columns. Every one of those figures is already stated twice on the page, in the KPI tile and again in the Breakdown panel, and the exact value is one hover away on the tooltip. Series names alone need less room, so the legend's `maxWidth` cap comes down from 200 to 160 and the ring keeps the space. * fix(styles): raise chart label contrast to the AA floor Axis ticks and legend entries are small text, so they owe 4.5:1. On light they inherited the `gauzy-text-color-2` step, which measures 3.99:1 on white; the replacement measures 6.08:1. On dark they were a 55% white step that read as grey against the canvas, now full white. * style(dashboard): rebuild the HR KPI tiles on flex The tiles were a three-column grid with the glyph and chevron each spanning both text rows. Replaced by one flex row — glyph, a label-and- value block, chevron — which is what the layout always was. The new `.kpi-text` wrapper carries `min-width: 0`, so a long figure shrinks in place instead of pushing the chevron off the tile, and the chevron pins itself to the trailing edge with `margin-inline-start: auto`. Accents now read the `gauzy-chart-*` tokens the charts paint with, so a tile and its slice are the same colour by construction. They were not: `--info` and `--accent` both resolved to the info step, making Profit and Bonus identical on the tiles while the chart drew Bonus in fuchsia. The tint fill behind each glyph goes with them — the tile already has its own surface and hairline to sit on. Also flattens the page's type scale to 0.75rem and stretches the panel row so the breakdown and the chart share the taller height rather than leaving a ragged bottom edge. * style(dashboard): align history dialogs with the shared table hub The records and profit dialogs opened from the HR KPI tiles carried their own table metrics, so they read as a different object from every other data table in the app. They now take density, surface, row treatment and pager from `gauzy/_gauzy-table-hub`, the same partial the documents, employees, invoices, payments, pipelines and tasks tables already use. Drops the local 11px gutter on `.table-scroll-container`: the cells carry their own horizontal padding and the card body supplies the outer margin, so that gutter only pushed the surface outline a column-width off the table it outlines. `profit-history.component.scss` @forwards this file, so the profit dialog picks all of it up unchanged. * feat(dashboard): render employee chart legends in HTML Chart.js lays each legend entry out as one run of text, so the doughnut's "name + figure" entries came out ragged: four rows whose numbers each started wherever the name before them happened to end. A canvas legend cannot lay out two columns, and whatever width it claimed was measured out of the same rectangle the chart draws in — which is why the ring needed a maxWidth cap to stop a long currency string pushing it into a corner. The new ga-employee-chart-legend renders the entries as DOM instead. The amount is pushed to the trailing edge so every figure ends on one right margin, and tabular-nums lines up the digits inside it. Being a CSS sibling of the canvas, it divides the panel with the plot rather than competing for it: the doughnut gets a box with the panel's full height and inscribes itself in the shorter side, so the ring comes out as large as the section allows and no larger. The doughnut's amounts are restored to its legend, where they were before the redesign. The stacked chart takes the same treatment for its fit, but keeps names only: its bars are normalised so each month's stack sums to that month's income, so a total printed beside a series name would not be the quantity any bar is showing. The horizontal bar chart still draws its legend on the canvas, so employeeChartLegend stays — minus its position parameter, whose only caller was the doughnut. * fix(dashboard): restore the visible calculation lines on the HR page ga-info-block, the component this page was built from, rendered its `meta` input as visible text under every title. The redesign kept all ten of those strings but moved each one onto a tooltip, so figures that used to be readable at a glance were reachable only by hovering — and one of them, the bonus percentage, appears nowhere else on the page at all. They are body text again: kpi-meta under each tile's figure, stat-row-meta under each breakdown label. The tooltips that carried them are dropped, since a tooltip repeating text already on screen is noise. The identity bar's edit button keeps its own tooltip: that icon has no visible label. The salary row is left without one — it is new to this page and never carried a formula — so its label stays a direct child of the row and takes the flexible width itself. * style(dashboard): drop the KPI tile formulas to 10px Splits the shared meta rule so only the four overview tiles shrink. A tile carries the same sentence in a quarter of a breakdown row's width, so it wraps to three or four lines where a row takes one; at 11px that turned the KPI band into the densest thing on the page. The leading comes down with the size. 1rem of it under a 10px face reads as separate paragraphs rather than one wrapped sentence. * fixed the spell error * fix(dashboard): rebuild the doughnut legend when the organization changes The tooltip formats its amounts in the callback, so it picks up a new organization's currency the moment the store emits. The legend does not: its amounts are strings built the last time the dataset was. Between an organization change and the arrival of that organization's statistics, the two disagreed — the legend still printing the previous currency beside a tooltip already using the new one. Rebuild the dataset in the organization handler so both switch together. * fix(dashboard): reach the HR empty state and track traits by position Two defects on the HR page. `loading` was only ever cleared inside getEmployeeStatistics(), which is reached solely through statistics$ — and that only fires downstream of a filter requiring a truthy employee. With no employee ever selected the spinner ran forever and the "no employee selected" state, gated on `!loading`, could not render. Clear the flag before that filter, on the settled emission that carries no employee. The identity traits are the employee's short description and level, and nothing stops those holding the same string, which gave the @for two identical keys and threw NG0955 instead of rendering the chips. Track them by index. * test(e2e): match HR cards on their label, not their whole body The note on infoBlockCss claimed each card's arithmetic sat on an nbTooltip, where Playwright cannot see it, so every queried heading matched exactly one card. That has not been true since the formulas became body text: the Profit tile prints "Profit (Net Income) = Total Income X - Total Expenses Y", so a whole-card hasText matched it for both the "Total Income" and the "Total Expenses" query. The clicks only landed correctly because .first() happened to return the DOM-first tile. Scope the filter to the label span, where each of the four headings identifies one card, and correct both comments. * chore(cspell): restore alphabetical order in the words list "deutan" sat after "devkit", "Gridlines" after "Guazy", and "uppercasing" among the "un" entries. Move each to its own place. * fix(dashboard): average the HR bonus over the selected range The divisor came from `employeeStatistics.length`, but the aggregation endpoint returns a row only for months that hold records — so a range containing an idle month averaged over its ACTIVE months and overstated "Average Monthly Bonus". Count the calendar months the picker asked for instead, both ends included. * refactor(dashboard): inject the employee chart dependencies The three employee charts took every dependency — including the newly added ElementRef — through constructor parameters, against the repo's Angular rule to prefer `inject()`. TranslateService drops out entirely: TranslationBaseComponent takes it optionally and injects it itself, so `super()` covers it. The empty constructors stay because a component that inherits a parameterised constructor from an undecorated base does not compile. * test(e2e): locate the HR chart switcher by its class The redesigned switcher declares no `placeholder`, so the old [placeholder="Chart"] locator matched nothing and the chart-options scenario timed out. Use the `.chart-switcher` class the panel header gives it — the same nb-select host the attribute sat on, so the click target is unchanged — rather than the translated aria-label, which would tie the locator to the active language. |
||
|
|
1f69869be0 |
Fix/polish organizations pages (#10140)
* fix(candidates): give the candidate profile its own stylesheet
`edit-candidate-main.component.ts` listed the Organization > Main tab's
stylesheet in its own `styleUrls`, so every selector in that file was compiled a
SECOND time carrying this template's `_ngcontent` attribute. The two templates
share no markup — only the file — which meant the organization tab could not be
restyled without silently restyling the candidate profile with it.
Copy across the rules this template actually renders: `.content`, the
`.organization-container` panel with its `.organization-photo` children (the
overlay `div`, the 100px `img`, the file `input` and the 68px `svg` placeholder),
the `.row .col` halving, and both responsive blocks.
Left behind, because this template renders none of them: `.main-form` (this one
uses `.employee-form`), `.employees-count-text`, `.tax-form-group`,
`.registration-form-group`, the `ga-currency` and `nb-select` overrides, `.col-6`,
and `.content.main` — the candidate's `.content` carries no `main` class.
Two declarations are dropped rather than copied, both no-ops here:
`width: min(563px, 100%)` on `.organization-container`, which this template
already overrides with bootstrap's `w-25` (`width: 25% !important`), and
`margin-left: 0 !important` on `.organization-photo`, which existed to cancel an
`ml-4` that only the organization template carried.
* fix(organizations): repair the edit-organization header content
Three things were wrong with what the card header rendered.
The headcount ran through the `json` pipe — `{{ organization?.totalEmployees |
json }}`. On a number that is a no-op, but `JSON.stringify(null)` is the STRING
"null", so an organization whose count had not been computed yet announced
"null Employees". It reads the value directly now, falling back to 0.
The logo was an unguarded `<img>`, so an organization with no image got the
browser's broken-image glyph and the alt text sitting in the header. Guard on
there being a URL and fall back to a briefcase icon, the same shape the employee
page uses for its own missing avatars. `logoFailed` also covers the case where a
URL is present but fails to load.
The name and the metadata beneath it were both plain lines of body text, so the
organization read no louder than its own headcount. Name and supporting copy are
now separate rows, with the count joined by official name and currency.
`setOrganization` exists so both subscriptions clear `logoFailed` — without that
a single broken image would leave the placeholder in place for every
organization selected afterwards. A back button joins the header, matching the
employee edit page.
* fix(organizations): restyle the edit-organization page header
The contact meta read louder than the organization it described. `Edit Public
Page` was `display: flex`, so the control stretched the full width of the header
and read as a banner rather than an action, and the organization name was 14px
600 — the same size as the headcount line beneath it.
Lay the header out as one row: back button, logo, identity, then the action
pinned to the trailing edge. The name takes a heading size and the supporting
copy sits under it as a wrapping muted row, dot-separated. `Edit Public Page` is
a real `<button>` now with a resting fill and a hover state, and the logo
placeholder is sized to match the image it replaces.
The old `@include respond(sm)` block stacked `.header-container` into a column,
which put the avatar above the name; the replacement wraps the action under the
identity instead and keeps the avatar beside it.
This stylesheet is NOT scoped to one template — `accounting.component.ts` lists
it alongside its own, so every selector here is compiled a second time against
the Accounting page. Everything above is nested under `.card-scroll`, a class
the Accounting card does not carry (its `nb-card` is `.card`), so none of it can
reach that page. `.body-header` and `.sub-header` stay exactly where they were,
for the reasons already recorded above them.
* fix(organizations): import SharedModule for the header back button
`EditOrganizationModule` declares `EditOrganizationComponent`, whose template now
renders `<ngx-back-navigation>`. That component is declared and exported by
`ComponentsModule`, which reaches this module through `SharedModule`; without the
import the element would be parsed as an unknown tag and silently render nothing.
* fix(organizations): repair headcount, logo and save behaviour on the Main tab
Three defects in this component, none of them cosmetic.
The headcount was destructured out of `route.parent.data`:
tap(({ employeesCount }) => (this.employeesCount = employeesCount))
but the parent route resolves only `organization` and `organizationTaskSetting`.
There has never been an `employeesCount` key to pick up, so the field stayed
undefined and the panel rendered a bare "Employees" with no number in front of
it. The count lives on the organization itself, maintained by the employee
subscriber, so read it from there.
Picking a logo threw you off the page. `updateImageAsset` called
`updateOrganizationSettings()`, which ends by navigating to the organizations
list — so choosing an image saved the form and then abandoned it. Split the save
in two: `saveOrganization()` persists and returns whether it worked,
`updateOrganizationSettings()` persists and then navigates. The upload path takes
the first, the Save button the second, which also means Save no longer navigates
after a failed request.
The form seeded `imageUrl` from the `imageUrl` column alone, while the card
header a few pixels above resolves `image.fullUrl || imageUrl`. An organization
whose logo came from an image asset therefore showed the logo in the header and a
placeholder in the panel. Both read the same expression now; `imageUrl` is a
disabled control, so this is display-only and never reaches the update payload.
* fix(organizations): rebuild the Main tab as an identity panel and a form
The left panel was a 563px box holding a 100px logo and one line of text, so
most of it was empty; the form beside it was a bootstrap `.row`/`.col` grid whose
fields did not line up, and the Save button was positioned by `margin-top: 60px`
with nothing to scroll when the form outgrew the tab.
Lay it out the way the employee Account tab is laid out: an identity panel and a
form panel, each a bordered flex column. The panel now carries the name, official
name, description, status chips and a facts grid — headcount, time zone, start of
week, date and time format, bonus rules — all guarded, so an organization with
none of them shows a header and nothing else. The logo gains a placeholder, a
hover scrim and a camera badge, so the upload affordance is visible whether or
not an image is set.
The form is a two-column CSS grid. Three fields used to size themselves
independently of it: `ga-currency` was `inline-flex` with a 180px floor and tax
ID was pinned to `width: 80%`, both of which left their columns visibly narrower
than the fields beside them. Tags spans the full row. The fields scroll and Save
sits in a bordered footer beneath them.
Two `::ng-deep` blocks were bare top-level selectors. Angular drops the scoping
attribute from those, so `ga-currency .form-group` and
`nb-select.shape-rectangle .select-button` were global rules applying to every
such element in the app, not just this tab's. Both are `:host`-scoped now.
The tab keeps `height: calc(100vh - 19.25rem)`, which is how the Location and
Settings tabs size themselves too; converting the shared tabset to a flex chain
is a change to all three, not to this one. What changes is that the panels now
scroll their own overflow inside it.
The panel fill was a literal `rgba(126, 126, 143, 0.1)`, which is one fixed grey
across all eight themes; it follows `--gauzy-card-3` now.
* fix(organizations): stop stranding the Settings tab at the far edge
The route tabs are laid out as a flex row, and the strip carried
li:last-of-type { margin-left: auto; }
An auto inline-start margin on the final flex item absorbs all the free space
before it, so with three route tabs "Settings" was pushed hard against the
opposite edge of the card while "Main" and "Location" stayed together on the
left. The three are peers — one group of tabs — and they now sit as one;
where that group goes is `justify-content`'s job, which the `.full-width` rule
below already sets.
The tab padding was declared only inside three nested max-width queries: `dsk`
(<=1532px) at 20px, a bare `1440px` at 25px and `xxl` (<=1280px) at 20px. They
share a specificity, so the later ones won where they overlapped and the
horizontal padding ran 20 -> 25 -> 20 as the window got NARROWER, while anything
wider than 1532px matched none of them and fell through to Nebular's own
padding. Replaced with one base value, narrowed once below `xxl`.
* fix(organizations): make the settings section index usable
The aside listing the ten settings sections had three problems.
Its markup was `<ul><span (click)="general.toggle()"><li>…</li></span>…</ul>`,
repeated ten times. A `<span>` is not a legal child of `<ul>`, and a span with a
click handler has no role, no tabindex and no Enter/Space handling — the entire
index was unreachable by keyboard. Each entry is now an `<li>` holding a real
`<button>`, and the ten hand-written copies collapse into a `@for` over a
`settingsSections` array declared beside the accordion refs.
Clicking an entry called `toggle()`, so clicking the section you were already
reading closed it. An index into a page should open, not toggle.
Nothing scrolled. The accordion is a single column of roughly two thousand lines,
so opening a section below the fold — Task Setting and Integrations are ninth and
tenth, behind a five-hundred-line Timer section — changed something entirely
off-screen and the lower half of the index looked inert. `openSection` now brings
the section it opened into view.
The ten `@ViewChild` accordion-item fields go with the old markup: they existed
only so the template could call `general.toggle()` and read `general?.expanded`,
and nothing reads them once the aside addresses sections by position. Two
`@ViewChildren` queries replace them — one for the items, one for their elements,
which is what the scroll needs. The `#general` template refs stay on the items as
labels; the `#generalBody` refs they sit beside are still read by each body's
`[hidden]`.
The row styling moves from the `<li>` to the button, which means handing back the
font, colour, border and text alignment a `<button>` overrides by default, and
adding the focus ring the list never needed while it could not be focused. On
small screens the list was laid out by setting `display: inline` on the `<ul>`
and hanging a `margin-right` off each item; it is a wrapping flex row now.
* fix(organizations): repair the settings page layout
Four measurements on this page disagreed with each other.
`.main-form` declared `overflow: hidden` and, three lines later, `overflow-y:
scroll`, while `.accordion-section` inside it declared `overflow: auto`. Two
nested scrollbars ran over the same content, and because the outer one moved
everything, the section index scrolled away from the sections it points at. The
accordion column is the only scroller now and the aside stays put beside it.
The column widths were stated three times and did not agree: a 180px aside, a
`.fields-section` of `calc(100% - 230px)`, and `justify-content: space-between`
to absorb whatever was left. That 50px discrepancy was the gap between the index
and the sections. The aside is a fixed 180px track, the sections take the rest,
and a real `gap` separates them.
Save sat ABOVE the accordion — the control that commits the page placed before
any of the fields it commits, scrolling out of reach on a page this long. It is
the foot of the column now, pinned below the scrolling sections and bordered,
matching the Main tab. The small-screen `order: 2` that used to flip it to the
bottom goes with it, since it is last in the markup.
`.col-6` was capped at `max-width: 49% !important` against bootstrap's own
`flex: 0 0 50%`, so every pair of half-width fields rendered 49% wide with a 2%
hole between them that no gutter accounted for.
Stacked below `md` the whole column scrolls as one, as it did before — the aside
is a wrapping row of ten buttons there, and pinning it would leave almost nothing
for the sections. The aside's flex basis is reset to `auto` in that block: the
form is a column there, so the 180px basis that sets its width on desktop would
otherwise set its height.
* fix(organizations): rebuild the Location tab to match its sibling tabs
The last of the three edit tabs still laid out as a bare 40%/60% split held
apart by `justify-content: space-between`, with no panels, no scrolling and the
Save button pushed down by the same trick. Give it the two bordered panels the
Main tab uses: address fields on the left, map on the right, fields scrolling
with Save pinned in a bordered footer beneath them.
`ga-location-form` places its fields on the bootstrap grid at widths picked for a
full-width form — col-11 for the search and both address lines, col-8 for country
and city, col-6 for the postcode. In a panel this narrow that read as five
different field widths stacked on top of each other rather than one column, so
every field now takes the panel's width. Latitude and longitude stay paired.
`leaflet.component.scss` pins the map to `calc(100vh - 30rem) !important`,
measured against the viewport rather than the box it sits in, so inside a panel
it either left a gap below itself or pushed out past the bottom. It fills the
panel now. Because that changes the map's box, `_setLocationFormValue` calls
`invalidateSize()` — Leaflet caches the container size when it builds the map, on
its own timer, and the method's docs ask callers who resize its box to do exactly
this.
Three rules went out with the rewrite. `.content` named nothing in this
template. `order: 1` on the map put it after a form already ahead of it in the
markup. And `:host ::ng-deep input { background-color: background-basic-color-1 }`
fought the card shell's own `input-appearance(42px, var(--gauzy-card-1, …))`,
winning on injection order — which is why this tab's inputs were the only ones on
the page that did not match the Main tab's.
`:host.ng-star-inserted` is now plain `:host`: `ng-star-inserted` is Angular's
marker for a node created by an embedded view, not an API to hang a layout on.
Save also gains the `type="button"` it needed to stop doubling as a submit.
* fix(organizations): repair the organization name cell in the list
This renderer draws the first column of the organizations list, once per row.
Its logo was a bare `<img src="{{ rowData.imageUrl }}">` with nothing guarding
it, so an organization with no logo drew the browser's broken-image glyph in its
row. It also read `imageUrl` alone, while the card header and the Main tab both
resolve `image.fullUrl` first and fall back to the column — so an organization
whose logo came from an upload had one everywhere except here. Guarded, resolved
the same way, and backed by a briefcase placeholder sized like the image it
replaces so rows without one keep the same rhythm.
The Default badge was painted one colour and lettered for another:
[style.background]="background(rowData?.color)"
[style.color]="backgroundContrast(rowData?.brandColor)"
`IOrganization` has no `color` — the fill was computed from `undefined` while the
text contrast was matched against `brandColor`. Both read `brandColor` now.
Two strings were hard-coded English in a translated app: `text="Default"` and
`Members count {{ n }}`. Both have keys already — `FORM.LABELS.DEFAULT` and
`ORGANIZATIONS_PAGE.EMPLOYEES`.
`rowData.tags.length` was reached without a guard, on a row object whose every
other access is optional; the list does load the relation, but this renderer is
not the one deciding that.
In the stylesheet, the logo's `0px 1px 1px rgba(0, 0, 0, 0.25)` drop shadow is
invisible against the dark themes and becomes `var(--gauzy-shadow, …)`, and the
member line's `11px`/`rgba(126, 126, 143, 0.75)` becomes the shared chip size and
the muted text ramp — that literal is the ramp at 75%, which read as a third,
dimmer grey beside every other muted label in the table.
* fix(organizations): tidy the remaining organizations list cells
`OrganizationsFullnameComponent` is gone. It was declared in `OrganizationsModule`
and re-exported from the table-components barrel, but the module declares no
`exports` and no template anywhere renders `ngx-organizations-fullname` — the
list's name column goes through `OrganizationWithTagsComponent`. It carried its
own copy of the unguarded-`<img>` bug fixed in that renderer, on a 70x63 tile
nothing drew, so it was dead code that still had to be read and kept honest.
The currency and employee-count cells each wrapped their value in
`<div class="m-2">`. That is a 0.5rem margin inside a cell the smart table already
pads, and the Name and Status columns add none — so those two columns' contents
sat half a step in from everything else in the row. The margin goes; the values
keep their weight.
* fix(organizations): drop dead CSS from the organization tabset
This component's template is two elements: `.org-settings` wrapping an
`<nb-route-tabset>`. Angular puts the component's content attribute on the
DESCENDANT half of a compiled selector, so a rule here can match neither an
ancestor nor anything inside the routed tab components below it. Five things in
this file were in one of those two positions.
`@forward '@shared/_edit-profile-form'` emitted 28 rules into this stylesheet,
every one rooted at `:host nb-card` or `:host nb-card-body` — an editing form's
chrome, including a whole `.employee-container` photo widget, in the file that
styles a tab strip. It re-exports mixins too, but none are used here and both
`var` and `themes` are already used directly.
`nb-card-body { overflow-y: hidden }` aimed at the card body in
`edit-organization.component.html`, which is this component's PARENT — it is
projected into that element, not the other way round.
`.form-group input`, `nb-tab`, `nb-tabset` and `.header` name nothing in the
template; the tabset element is `nb-route-tabset`, which the `::ng-deep` block
below reaches, and that is exactly why that block works where these did not.
Verified by compiling the file before and after: 45 rules to 13, 5203 bytes to
1713, with every removed selector rooted at `nb-card`, `nb-card-body`, or one of
the four names above, and nothing new appearing.
* fix(organizations): let the organization card fill the layout column
The empty band between the card and the footer on
/pages/organizations/edit/:id had one cause: `:host nb-card { height: unset }`
in this stylesheet compiles to (0,2,1) and silently outranked the global
`.card-scroll { height: calc(...) }` (0,1,0) from styles/_overrides.scss. The
card therefore sized to its CONTENT -- a header, a tab strip, and a tab body
that measured itself off `100vh` -- and always came out shorter than the column
it sits in.
The shell already hands the page the right box: `nb-layout-column` is a flex
column and `> router-outlet + *` (the routed page component, i.e. this `:host`)
gets `flex: 1 1 auto; min-height: 0`. That height just was not being passed on.
The card now takes it and hands it down through its body, which is the first
step of a continuous chain -- column -> host -> card -> body -> tabset -> tab.
`height: unset` is kept for `nb-card:not(.card-scroll)`, because this file is
also listed in `accounting.component.ts`'s styleUrls and Accounting's nested
cards still rely on it; that page has no flex chain to inherit a height from.
Also drops the header's `25px 20px 0` to `1rem 1.25rem 0.75rem`. The band above
the tab strip was the largest block of empty space on the page and it is chrome,
not content.
* fix(organizations): size and shape the organization tab strip
Two things, both about the tabset element itself.
The height. `nb-route-tabset`'s template is `<ul class="route-tabset">`
followed by a bare `<router-outlet>`, so the routed tab component is a SIBLING
of the strip inside this host -- which makes this the place to close the chain
the card now starts. The host, `.org-settings` and the tabset become a flex
column and the routed tab gets `flex: 1 1 auto; min-height: 0`, so each tab is
handed the space left under the strip instead of measuring the viewport for
itself. The outlet element is hidden: it is an anchor for the routed view, and
as a flex item it would take a share of the column.
The tabs. `.tab-text` was 16px/600, sitting on the inner `<span>` where it beat
both shared tab rules -- `route-tabset-tab-text-font-size` (0.875rem, weight
400) and the active-tab weight from `nb-tabset-overrides()` -- so every tab on
this page was a size larger and a weight heavier than the tab bars everywhere
else in the app, active or not. It now sets only what the shared rules do not.
Padding goes from 10px/25px to 7px/16px and the icon from 18x19 to 15px, both
sized for the type rather than for the 16px it used to be, and the link takes
the top half of the card's radius so the active tab and the panel under it read
as one folder.
The small-screen release moves from `sm` (480px) to `lg` (991px), where the
three tabs actually stack their panels. Between the two there was a 500px band
in which the tabs had stacked but nothing had stopped constraining them.
* fix(organizations): drop the 100vh guess from the three organization tabs
Main, Location and Settings each sized themselves with
`height: calc(100vh - 19.25rem)` -- three copies of one estimate of the page's
chrome, and an estimate that cannot be right in both states: the demo-account
banner alone puts the header band about 23px over the theme's `header-height`,
so the tab was that much shorter than the space it had. That is the other half
of the empty band under the card.
The card is a real frame now and the tabset passes its height down, so each tab
takes what it is given -- `flex: 1 1 auto; min-height: 0` on the host, and the
tab's own panel row fills it. Same box in all three, from one source.
Also removes `:host { nb-card-body { overflow: visible } }` from the Settings
tab. The card body belongs to `edit-organization.component.html`, two components
up, and a component stylesheet cannot match an ancestor -- this compiled to
`[_nghost-x] nb-card-body[_ngcontent-x]` against a template that renders no card
at all. It is called out rather than quietly deleted because the card body is
now the page frame's last flexible step.
* fix(organizations): put Registration Date and Tags on one row
The tags field carried `org__field--wide`, whose only rule was
`grid-column: 1 / -1`. In an eight-field, two-column grid that left Registration
Date alone on its row with a column of empty space beside it and then gave Tags
a row of its own -- two half-empty rows for two fields that pair. They share a
row now: eight fields, four rows, no holes. The rule went with it, since tags
was its only user.
Pairing them means the two halves have to agree, and they did not.
`ga-tags-color-input` renders its label and `ng-select` inside a plain `<div>`
rather than a `.form-group`, so nothing the grid says about a field reached it.
It is given the same column shape as its neighbour, and the label gap is stated
once for the whole grid -- two of the eight fields bring their label from a
child component (`ga-currency` and now `ga-tags-color-input`) while the other
six declare it inline, so the space under a label was previously whatever each
one happened to inherit.
* fix(organizations): lay the organization settings fields out on one grid
Every section of this form uses the bootstrap grid, and every section also
decided its own measure on top of it: General's fields were 75% of the section
body, its Time Format select 30% of its own half and its two time pickers 60% of
theirs; Design was a pair of 140px selects; Accounting's fiscal years a 400px row
of 170px pickers and its toggles a 500px column; Date limit a 50% row. Nine
widths down one column of one form, so no two sections began or ended at the same
x and almost nothing lined up with the field above it.
One measure now -- the section body -- divided by the `col-*` classes the markup
already carries. The four fields that use no column at all (Design's selects,
the fiscal years, and the bare `.form-group`s in Accounting and Date limit) take
the same halves through their own wrappers.
The rest of the misalignment was smaller and more mechanical:
- The gutter disagreed with itself. Bootstrap's is 30px (`.row { margin: 0 -15px }`
against `.col-* { padding: 0 15px }`) and this file narrowed only the column
half, to 10px, so every row of fields began and ended 5px outside its panel.
Both halves are now declared together.
- Rows had no vertical rhythm. `.row` wraps, so one row in the markup is usually
two or three on screen, and with `.form-group { margin-bottom: 0 }` those sat
flush against each other.
- General's rows were `align-items: center`, which centres a short field against
a tall one and takes its label out of line with the label beside it.
- Four info buttons sat OUTSIDE their label, between it and the control, each
pushing its own field a full button-height down the row. They are inside the
label now, and a label with one is no longer taller than a label without.
- "Standard Work Hours Per Day" was wrapped in a `col-6` nested inside a
`col-xl-6 col-12`: half of a half, a quarter-width field beside full-width
toggles.
- Time Zone sat low because `ga-timezone-selector` brings its own `.form-group`
(keeping bootstrap's 1rem margin) and its own label, neither of which a rule
scoped to this component can reach.
- General's selects were painted `background-basic-color-1 !important`, so ten
controls in the first section were a different colour from the same controls
in the other nine. They all take the card shell's `input-appearance` now.
The per-section rules this replaces were also addressed by POSITION, and had
already gone stale: an `#agent` section was added at index 8 and every
`:nth-of-type` rule from there down slid onto the wrong section, so the pair
meant for Task settings landed on Agent and Integrations lost its own. Nothing
here counts sections any more.
* fix(ui-core): carry a registered tab's router-link inputs onto its tab link
`DynamicTabsComponent.getRegisteredNbTabs()` mapped a `PageTabRegistryConfig`
onto an `NbRouteTab` by copying six fields — title, tabId, route, icon,
responsive and activeLinkOptions — and silently dropped the rest. Seven of the
dropped ones are inputs `nb-route-tabset` binds on every tab link:
`queryParams`, `queryParamsHandling`, `fragment`, `preserveFragment`,
`skipLocationChange`, `replaceUrl` and `state`.
So a tab that asked for `queryParamsHandling: 'merge'` navigated with
`undefined` instead and lost the page's query parameters on every switch. The
Time & Activity page registers all five of its tabs that way
(`employees/activity/layout/layout.component.ts`), and nothing said otherwise.
Also widens `PageTabsetPageId` with 'organization-edit-page', for the tabset
the organization edit page registers next.
* fix(organizations): render the organization tabs through the shared tabset
The organization edit page hand-built an `<nb-route-tabset [tabs] fullWidth>`
and then restated the whole tab bar locally under `:host ::ng-deep`, where no
shared rule could reach it: a 7px/16px tab box against the theme's
0.75rem/1.25rem, 15px icons with a 6px gap, an `!important` svg fill fighting
the `currentColor` chain, `letter-spacing: 0.02em` on `.tab-text`, its own copy
of the active fill, and a `display: flex !important` that suppressed the shared
stacked icon layout at every width. The result was a tab bar visibly smaller
and tighter than every other tab bar in the app.
It now renders `<gz-dynamic-tabs class="tabset-container">` over the page tab
registry — the same component, the same registry and the same template the
employee edit page uses — and registers its three tabs as `PageTabRegistryConfig`
instead of building `NbRouteTab[]` by hand. Nothing here styles a tab link any
more; the strip takes `includes/_tabset.scss`, `nb-tabset-overrides()` and
`nb-overrides()` like every other tabset. The compiled CSS for the strip and its
container chain is byte-identical to `edit-employee-profile.component.scss`.
Two things went with it. `fullWidth` sets Nebular's own
`:host(.full-width) .route-tabset` to `space-around`, which a local
`justify-content: flex-start !important` then had to undo — and that override
was written as a bare `::ng-deep` with no `:host`, so Angular emitted it
globally and it leaked onto the five plugin pages that do use `fullWidth`, but
only once this page had been visited. And `flex-wrap: wrap` on the strip is
replaced by the shared horizontal scroll port with the scrollbar hidden in all
three engines.
* fix(organizations): give the organization edit card the employee card's frame
Two differences from the employee edit card, both visible on the tab strip.
The card body kept Nebular's default `overflow: auto` where the employee card
is `overflow: hidden` (via `nb-card_overrides(hidden, …)`). The tabs own the
scrolling — each has a scroll port over its own fields — so the card body had
nothing to scroll and that `auto` only ever produced a SECOND scrollbar, down
the full height of the body and alongside the tab strip, on top of the real one
inside the panel. It is `hidden` now; the below-`lg` media query still hands the
overflow back, so a stacked tab cannot be clipped. The body is also a flex
column, so the settings component takes its height as a flex item the way
`ngx-edit-employee-profile` does.
The card also painted itself `gauzy-card-2` — the exact colour `nb-overrides()`
paints the ACTIVE route tab — so the selected tab was the same colour as the
strip it sat in and only its text weight marked it. The card takes the theme's
own `card-background-color` now, which is what makes the active tab read as a
folder tab lifted off the header; each tab still paints its own `gauzy-card-2`
panel, so the tint stays where it belongs.
Also drops `ngx-back-navigation` from the card header, and with it the
`SharedModule` import that existed only to provide it — nothing else in this
template comes from that module.
* fix(organizations): put the Location and Settings tabs on the shared panel scale
The Main tab already matches `@shared/_employee-tab-panel` step for step. Its
two siblings did not.
Location was on the same system but compacted at `$break-sm` (480px) where Main
and every employee tab compact at `$break-md` (767px), so between those widths
it was the only tab on the page still holding desktop padding — and its Save row
never tightened at all. It takes the `md` step now, actions included.
Settings was on a px chrome of its own:
* the tab box framed its panels with `gap: 20px; padding: 20px` and the
trailing side cut to 8px, against the `1rem` on all four sides its siblings
use, so its content began at a different x. The 8px bought nothing — the
scroll port below keeps its own gutter;
* the section index had the card-3 fill and the radius of a panel but no
hairline, `13px 18px` of padding, and `height: fit-content`, so it stopped
at its last item while the sections beside it ran to the bottom of the row;
* its heading was 14px/600 — the same size as the items under it — rather than
the shared panel legend `.tab__legend` and `.org__group-title` both use;
* an accordion section is this tab's panel, and had no hairline either, so ten
stacked sections read as one undivided block;
* Nebular renders `<div class="item-body">` INSIDE `nb-accordion-item-body`
and both carried 15px, so a section's fields sat 30px in from the panel edge
against the 1.25rem every other panel uses. The host is 0 now and the
padding lives on the box that holds the fields. The `lg` rule that used to
zero the host — a no-op once the host is 0 — narrows `.item-body` instead.
Nav items, list gaps and the `md` step follow the same rem scale.
* fix(organizations): rebuild the settings tab as one panel of section rows
The ten sections were ten cards: a `gauzy-card-3` fill, a hairline and the card
radius each, stacked on the `gauzy-card-2` tab body. On the light themes those
two surfaces composite to #fcfcfc boxes on a #f9f9f9 ground — three values apart
out of 255 — so the frames never read as frames. The right-hand column was a
ladder of faint near-identical bars with nine redundant edges down it, while the
index beside it was a single solid panel: two columns of one form built out of
different things.
`.fields-section` is the panel now, put together the way `.org__panel--form` and
`.tab__panel` already are — panel -> scroll port -> foot:
* a section is a row, told apart from the row above it by a hairline. Nothing
in the accordion paints a surface, draws a box or reserves a gap any more,
which also means `nb-accordion-item` had to be told to let the panel through:
Nebular's default theme paints one a literal `#fbfbfb` and `gauzy-dark` a
sidebar token;
* open is painted as a state. The open row takes the shared hover tint at rest
— the same fill the index gives the entry pointing at it — with its title at
full strength and a hairline under it. Before, an open section and a closed
one were the same box with more inside it;
* the header answers the pointer. It IS the control that opens its section, and
it had no hover state at all; Nebular also host-binds `tabindex` onto it and
then strips the ring back off with its own `:host:focus { outline: 0 }`;
* Save is the panel's foot, `0.875rem 1.25rem` over a hairline, the box
`.org__actions` and `.tab__actions` are. It was a bare row on the card body
with a rule across the top of nothing.
The section index gives up its ten raised chips — each a `gauzy-sidebar-background-3`
fill, i.e. a SIDEBAR token inside a card, under a `--gauzy-shadow` that resolves
to a black drop shadow on six of the eight themes — for the shared flat-chrome
tokens every other list-of-links in the app uses: `gauzy-radius-sm` for the row,
`gauzy-hover-tint` for hover, `gauzy-active-tint` plus a primary rail for the row
you are on. Its two states used to be told apart by exactly one thing, since
hover and active both painted `background-basic-color-1`. It is also sized to its
content rather than stretched down the column, so ten short rows stop sitting
inside a panel drawn the whole way down the viewport.
Colour and metric fixes that came with it:
* `.item-body` was targeted without `::ng-deep`. That div belongs to Nebular's
template, so it carries Nebular's content attribute and never this
component's, and Angular stamps this component's onto every compound selector
that is not behind `::ng-deep` — the rule matched nothing, and every section's
fields started 10px to the left of the section title they belong to;
* the select trigger carried `background-color: background-basic-color-1
!important` plus a `margin-right: 10px` on a `.select-button` already set to
`width: 100%`, so every select ran 10px past its own field;
* the unchecked toggle track was a solid mid grey, which at a glance reads as a
toggle that is on. It takes `toggle-basic-background-color` with
`toggle-basic-border-color` for the edge;
* `::ng-deep .ng-value` was written bare at the start of a selector list, which
drops the scoping attribute — one tab of the organization editor was setting
`z-index: 2` on every selected value in every `ng-select` in the app;
* the chevron sat at Nebular's `right: 1rem` against a header padded to
1.25rem, and was the only icon on the page drawn inside a ring;
* `pt-2` on seventeen columns and `mt-2` on three stacked on top of the grid's
own rhythm, so the longest section ran 1.5rem loose against 1rem elsewhere;
* `.select-wrapper` took the row rhythm twice, and the last-row release could
not reach `.design-select`, `.year-pick` or the bare `.form-group`s, so those
sections closed with a 2.25rem gutter against every other section's 1.25rem.
* fix(ui-core): paint the edit-card fields on the surface they actually sit on
Both edit cards passed `gauzy-card-1` to `input-appearance()`. That is the CARD's
surface, the bottom of the elevation ramp, and it is the right fill for a field
that sits on the card — but no field on either page does. Every organization tab
and every employee tab puts its fields on a `gauzy-card-3` panel, two steps up
that ramp: card-1 -> the `gauzy-card-2` tab body -> the card-3 panel.
On the light themes the mistake is invisible, card-1 being white against a card-3
that composites to about #fcfcfc. On `gauzy-dark` it is the whole problem: card-1
is an opaque `rgba(18, 18, 20, 1)` while the panel composites to about #202022,
so each field read as a black hole punched through its panel rather than as a
control resting on it. `gauzy-card-4` is the next step up from the panel, which
is what a control on a card-3 surface should be — 6 % white over the panel's 4 %
on `gauzy-dark`, 75 % over 50 % on the light themes — and it is already the
choice `job-search.component.scss` makes for the same situation. The old value
stays as the fallback for any theme that does not emit card-4.
The height stops being a hardcoded `42px` and takes `$default-height`, which is
`calc(select-medium-text-line-height + input-medium-padding-y * 2)` — one line
box plus both block insets, i.e. the height `nb-select` computes for ITSELF out
of the density tokens, and 2.375rem at the current preset. The literal predates
that preset, so the mixin's `min-height: … !important` was propping every field
4px above the box its own control had already worked out, and the two would drift
further apart the next time the density tokens move.
The organization call is scoped to `.card-scroll` rather than replacing the one
on `:host`. That stylesheet is also listed in `accounting.component.ts`'s own
`styleUrls`, so everything in it compiles a second time against that template,
and Accounting's fields DO sit directly on its card, where card-1 is correct.
`.card-scroll` is on this page's `nb-card` and not on Accounting's, which is the
hook the rest of that file already uses to mean "the organization editor only";
at (0,2,2) against (0,1,1) it wins both arguments and leaves every other
declaration the mixin emits identical. `edit-employee.component.scss` has no such
second template — it is that component's alone — so it is changed in place.
* fix(employees): give the settings tab the organization settings design
The two pages present the same thing — a long form split into collapsible
sections with an index into them — and were built out of different parts. This
sheet is now the counterpart of `edit-organization-other-settings.component.scss`
rule for rule: compiling both and diffing the analogous rules leaves 23 of 25
byte-identical, and the two that differ do so only in where the value sits (the
field inset lives on `.item-body` there and on `.settings__fields` here, because
that page is stuck on bootstrap rows while this one has a grid).
* ONE panel, not four. The sections were four cards on the card-2 tab body,
which on the light themes is #fcfcfc boxes on a #f9f9f9 ground. They are rows
of `.settings__sections` now, divided by a hairline, with the open row taking
the shared hover tint and a divider under it, and the header answering hover
and keyboard focus;
* Save is the panel's foot rather than a bare row that scrolled away with the
sections, which also makes this tab a fixed frame: `:host` drops the
`height: auto` / `overflow-y: auto` pair it shared with `employee-tab-host`
and takes the definite height the tabset already hands it;
* the index drops its 12.5rem track of outlined pills — each reserving a
transparent 1px border so the active one could swap it for
`color-primary-transparent-default` — for the shared flat-chrome tokens, and
is sized to its content instead of stretched down the column, where four
entries sat inside a panel drawn the whole way down the tab;
* the chevron loses the ring drawn round it on every row and moves to the x the
section title starts at, Nebular having pinned it at its own `accordion-padding`;
* a toggle is a cell of the field grid, so it is drawn as a control: the same
fill, the same `$control-hairline` inset ring and the same `$default-height`
floor as the input beside it. It was a transparent box outlined in
`gauzy-border-default-color` — the hairline meant for dividing a panel from
the page, not for drawing a control — which on the light themes is a #f0f0f2
line around nothing on a #fcfcfc panel;
* every toggle on this page carries an info button, and `nbButton` brings a
control-sized box with it, so it was the tallest thing in the box — taller
than the 24px text line and much taller than the 16px switch. Clamped, the
way the field labels' buttons already are;
* `.settings__fields` had `padding: 0 1.25rem 0.25rem`, i.e. no top inset at
all, so the first row of fields started hard against the header above it. It
takes the 1.25rem `.tab__fields` uses, and `gap` now does both halves of the
rhythm so there is no per-field margin to release under the last row;
* the local `.ng-select-container, .select-button { background-color:
var(--gauzy-card-1) !important }` goes; the card shell hands every control
under it one fill.
* fix(employees): open a settings section from the rail instead of toggling it
Each rail entry called `toggle()` on its accordion item and stopped there, which
had two consequences. Clicking the section you were already reading closed it,
leaving the rail with nothing marked active while that section's fields were
still the ones on screen. And because the sections are one scrolling column,
opening anything below the fold moved nothing into view, so the lower entries
looked inert.
The rail is an index into the page, so it opens rather than toggles and brings
the section it opened with it — the same behaviour, and the same implementation,
as the organization settings rail.
The item is matched to its element through two `ViewChildren` queries over
`NbAccordionItemComponent`, one read as the component and one as an `ElementRef`.
They walk the same template in the same order, so an item's position in one is
its element's position in the other; that is what lets the existing per-section
template refs keep addressing the rail entries by name.
* fixed the spell error
* fix AI comments
* style(organizations): tidy settings layout styles
* style(organizations): format settings components
* fix(organizations): address review findings
- Remove unused Input import from OrganizationWithTagsComponent
- Add missing id="settings-section-taskSetting" so the aside Task
Settings button's aria-controls resolves to a real element
- Declare postcss-scss devDependency required by .stylelintrc.json
customSyntax
* build: add postcss-scss to yarn.lock
package.json declared postcss-scss without a matching lockfile entry,
so CI's `yarn install --frozen-lockfile` failed with "Your lockfile
needs to be updated".
|
||
|
|
d36dc32b3e |
Merge pull request #10130 from ever-co/fix/polish-jobs-pages
Fix/polish jobs pages |
||
|
|
382b9f9621 | fix(ever-async): serialize setup and harden connector credentials | ||
|
|
625d621f19 | feat(integrations): complete organization-scoped Ever Async connector and builds | ||
|
|
17125da2e8 |
Fix/polish employee edit page (#10115)
* fix(employees): put the verified tick after the employee name
The badge preceded the name inside `.employee-details`, a box carrying
bootstrap's `flex-column` but no `display: flex` — the class sets
`flex-direction` only, so the container never became one. Its placement was
therefore whatever inline flow happened to give it, rather than anything the
markup asked for.
Wrap the name and the tick in a `.employee-name-row` that owns its own
alignment, with the tick trailing the name the way a verified badge reads
elsewhere. Re-indent the surrounding block, which had drifted a level out of
step with its nesting.
* fix(employees): restyle the edit-employee page header
The contact meta read louder than the employee it described. `Edit Public
Page` was `display: flex`, so the control stretched the full width of the
header and read as a banner rather than an action; username and email sat
under it as full-width lines of body-sized text, giving the email more visual
weight than the employee's own name.
Lay the three out as one wrapping row of supporting copy, sized down and
indented to start under the name above them. `$meta-indent` spells out the
three widths it follows — back button, avatar, gap — so resizing the avatar
does not silently break the alignment.
`.transparent` was a flat `opacity: 0.7` over the whole line, which dimmed
label and value equally; it is now a muted label with the value carrying the
weight. Also gives the public-page pill a hover state, sizes the avatar
placeholder icon, and puts one gap between the two header rows in place of
whatever margins their children brought.
The directional rules are scoped under `:host` on purpose. `nb-ltr`/`nb-rtl`
emit a `[dir=ltr]` PREFIX on the whole selector, and Angular's style scoping
only leaves that prefix alone when the selector also carries `:host` —
without it the scoping attribute lands on `[dir=ltr]` itself, which is on
`<html>` and never matches, so the rule silently does nothing.
* fix(employees): show a placeholder when an employee has no profile photo
The account tab's `<img>` was guarded on the form existing rather than on
there being a URL to load, so an employee without a photo got the browser's
broken-image glyph and the alt text sitting in the summary panel. Guard on
the value and fall back to a person icon, matching what the page header
already does. The fallback carries the same hover handlers, so the upload
affordance still works with no photo set.
Also names two elements the stylesheet needs to reach — the vetted badge and
the optional-stats list — and moves the badge's inline `position: relative`
out of the template with them.
* fix(employees): restyle the employee account tab
The summary panel had no padding, radius or edge, and stopped at its own
content, so it sat as a short grey stub against a tall empty tab. Give it the
chrome that makes a panel read as one and let it stretch to the height of the
form beside it.
The photo box had no size of its own — only the image did — so with no photo
it collapsed to the width of the alt text, taking the upload glyph that
centres on it off the corner of the avatar. Size the box instead, at 112px.
The optional stats each carried `margin-top: 20px`, opening four uneven gaps
down the panel; they are a divided list now. The save row sat on the default
form-group spacing and crowded the last field, so it gets a rule above it.
Adds `md` stacking as well: the 25/75 split leaves neither column usable on a
narrow screen.
Everything is scoped through `:host` deliberately. This component also loads
`edit-organization-main.component.scss`, whose rules are all written that
way, so a bare `.content { … }` here loses to it — which is why the old
`.content { padding: 20px }` never took effect and the tab was laid out on
the organization page's padding all along. Matching that specificity means
source order decides, and this file is listed second in `styleUrls`.
* fix(ui-core): let a label point at the language selector
ng-select generates no id of its own, so every `<label for="...">` next to
this component was an orphan: clicking it did nothing and screen readers
announced the control unlabelled. `labelForId` is passed through to the
ng-select template; nb-select has no equivalent hook.
* fix(ui-core): rebuild the language option as a flag/name row
Both templates rendered a bare `<img>` next to a text node, aligned with
`vertical-align: middle` — which aligns to the middle of the parent's
x-height rather than the line, so the flag sat high against the name and the
gap between them moved with the font. Each option is now one flex row, and
the name can take an ellipsis instead of pushing the trigger's arrow out.
Also here: `class="language-select"` is the only handle a stylesheet has on
the panel ng-select appends to `<body>`; `[size]` is dropped from the
ng-select branch, which declares no such input, so the binding only ever
wrote a stray DOM property.
* fix(ui-core): style the language option row
The flex row the templates now render: the flag keeps its 3:2 box and the
name takes the ellipsis. Drops `.language-color` and `.language-label`,
which named nothing in the template — the colour swatch they styled was
replaced by the flag.
* fix(ui-core): size the language dropdown panel like its trigger
Two global rules were built for a different kind of select and caught this
one by accident. `nb-overrides()` pins every image in every ng-select panel
to a 24x24 square, right for the avatars and org logos it was written for
but not for 3:2 country flags: they were blown up and cropped square, taking
each row from ~32px to ~36px and leaving the open list heavier than the
control it belongs to. And the `max-content` widening exists for a trigger
too NARROW to show its options — a filter cell in a table row; a form field
is the opposite case, so on a long option ("Limburgish, Limburgan,
Limburger") the panel spilled over the column beside it.
The panel is excluded from the widening rather than overridden: ng-select
writes the correct width INLINE, and an inline style cannot beat an
`!important` rule, so the only way to let it stand is for the rule not to
match. Both rules live here and not with the component because a component
stylesheet cannot reach a panel appended to `<body>`.
* fix(employees): label the preferred-language field
The account tab's `<label for="preferredLanguage">` had nothing to resolve
to until the selector grew a `labelForId` hook.
* fix(employees): make the edit-employee card body a flex column
The card override gives the body a definite height, but it stayed a plain
block, so the profile tabset inside it laid itself out at content height and
the height the body actually had went nowhere. First step of the chain that
lets a tab shorter than the card fill it.
* fix(employees): carry the card body height down to the routed tab
Nebular's `nb-route-tabset` template is `<ul class="route-tabset">` followed
by `<router-outlet>`, so the routed tab renders as the outlet's SIBLING and
`> router-outlet ~ *` is how a stylesheet addresses it. The outlet itself is
taken out of the flow so it is not a zero-height flex item between the strip
and the tab.
`min-height: 0` at every step because a flex item's automatic minimum size
is its content, which is what turns a long tab into a page-level scroll
instead of a scroll inside the card. Mirrors the same chain on the dashboard
tabset. Applies to every tab in the tabset, not only Account — they all
stopped at their own content height.
* fix(employees): fill the account tab and finish the summary panel
The tab measured itself against the viewport (`max-height: calc(100vh -
20.5rem)`), a second guess at the page chrome competing with the card's own;
when the two disagreed — as they do whenever the header collapses to the
"select an employee" line — the tab stopped short of the bottom of the card
and left a band of empty canvas under the form. It is a flex item of the
tabset now, so it takes the height the card body has left over.
Filling it is not enough on its own to SEE it: `.content` paints
`gauzy-card-2`, `rgba(255, 255, 255, 0.02)` on the dark theme, invisible
against the card behind it. The one element that shows the fill is the grey
summary panel, and it was opting out of stretching — `align-items: stretch`
only stretches an item whose cross size is `auto`, and the organization
stylesheet gives it `height: 100%`, which then resolved against a
`min-height`-sized parent and fell back to content height. As `auto` it runs
the full height of the tab.
The rest is the panel's own chrome: the avatar box back to its long-standing
100px, a placeholder fill that reclaims the box from the scrim rule painting
every div in it solid black, and the upload glyph down from 68px — over a
100px avatar it covered two thirds of the photo and, pinned to a permanent
0.3 opacity, read as something broken rather than as a control.
* fix(employees): grow the account tab into the card body
`.content` held its floor with `min-height: 100%`, a percentage against a host
that is `height: auto` — its size comes out of flex layout on the tabset, not
out of anything the cascade can see there. The percentage fell back to zero, so
the tab was laid out at content height and left a band of bare card body under
the Save row, with the summary panel stopping short beside it.
`flex: 1 0 auto` says the same thing in the language the host speaks: it is
already a flex column, so the tab's leftover height goes to its only item.
Shrink stays at 0 because the host scrolls — a form taller than the tab keeps
its own height and scrolls inside the host rather than being squeezed to fit.
* fix(employees): uncap the networks and hiring tabs
Both carried `max-height: calc(100vh - 20.5rem)`, a second guess at the page
chrome alongside the card's own. The two disagree the moment the page header
collapses to the "select an employee" line, and the smaller one wins: the tab
stopped short of the bottom of the card and left a band of bare card body under
the form. The tabset already hands each routed tab the height the card body has
left over, so there is nothing here left to measure.
Both also load `edit-organization-main.component.scss`, whose `height: 100%` on
the host made the flex basis a full tabset height — one the tab strip has
already spent part of — so the tab only came out right by being shrunk back
down. `height: auto` reclaims it, and the form card grows into the tab so the
surface reaches the bottom.
* fix(employees): size the employment card from the tab, not the viewport
`nb_card_overrides` was passed `calc($default-card-height + 0.25rem)`, which put
a viewport-derived height on the inner card body — a second guess at the page
chrome alongside the card's own. The two disagree once the page header collapses
to the "select an employee" line, so the inner card was sized to a height the
tab does not have: it scrolled for no reason on a short form and still left bare
card body under a tall one.
Passing `auto` lets the body take what the flex chain gives it. The mixin's own
`height: 100%` on the card is a percentage of a host sized by flex layout, i.e.
of nothing the cascade can resolve here, so the card grows into the tab instead.
* fix(employees): fill the location and rates tabs
Both carried `height: calc(100vh - 20.5rem)`, the same second guess at the page
chrome that left the other tabs stopping short of the bottom of the card. The
tabset already hands each routed tab the height the card body has left over.
`ga-employee-location` and `ga-employee-rates` are shared with the candidate
pages, so their own `height: 100%` — a percentage of a host sized by flex
layout, which the cascade cannot resolve — is overridden from these wrappers
rather than in their stylesheets, leaving the candidate pages as they were.
Location also gains the tab fill: it only ever coloured its own `nb-card`, and
that card carries an `m-3` body, so the margin around it showed the card body
straight through.
* fix(employees): fill the contacts and projects tabs
Both membership tabs carried `height: calc(100vh - 20.5rem)` on the host, the
same second guess at the page chrome as their siblings, and then asked their
`.container-*` panel for `height: 100%` of it — a percentage of a box whose own
size comes out of flex layout, which resolves to nothing the cascade can use.
The host is a flex column now and the panel grows into it, so the surface
reaches the bottom of the card body. Projects also picks up the `overflow-y`
its sibling already had, so a long list scrolls inside the tab.
* fix(employees): scroll the settings tab where every other tab scrolls
`.main-form` was pinned to `calc(100vh - 19.25rem)` — a second guess at the page
chrome alongside the card's own — and the host asked for `height: 100%` of a
tabset sized by flex layout, a percentage the cascade cannot resolve. Between
them the form either stopped short, leaving bare card body beneath it, or ran
past the bottom of the card.
The form grows into the tab instead. Its `overflow: hidden` and the
`max-height: calc(100% - 3rem)` cap on `.accordion-section` both hung off that
fixed height and are gone with it: they were what made this the one tab that
scrolled inside its own accordion column. The tab host is the scroller now, as
on the other eight.
Trade-off worth naming: the section nav beside the accordion no longer stays
pinned while the sections scroll. Consistency with the rest of the tabset is the
reason; if the pinned nav is wanted back it should come from a flex basis on the
column, not from viewport arithmetic.
* fix(employees): give the documents tab a surface of its own
It carried no styles at all, so it was the one tab with no fill: the links panel
floated on bare card body, and on a deployment with the Documents feature off
the panel renders nothing and the card body showed through for the tab's whole
height.
Same fill and flex column as the sibling tabs, with the panel growing into the
height the tabset hands the tab. `gz-document-links-panel` is shared with the
invoice and estimate pages, so it is sized from here rather than from its own
stylesheet.
* fix(employees): lift the tab scrollbar off the card edge
Every tab now scrolls at the routed component, so its scrollbar ran down the
very edge of the card body — over the card's rounded corner, hard against the
frame. `> router-outlet ~ *` is the one selector that already addresses every
routed tab, so the inset lives there rather than in ten stylesheets.
It has to be a margin: a scrollbar is laid out at the border edge of the scroll
container, outside the padding box, so `padding-inline-end` would only push the
content away from a bar that stayed where it was. 0.5rem is half the 1rem margin
the tabs' inner cards carry, which puts the bar in the gutter beside the card
rather than on top of its edge.
Logical property rather than the `nb-ltr`/`nb-rtl` pair: those emit a `[dir=ltr]`
prefix that only survives Angular's style scoping when the selector also carries
`:host`, which this nested `::ng-deep` selector cannot do.
* fix(rich-text-editor): fit the toolbar on one line and match its control heights
The `standard` preset puts 25 buttons and 2 selects in one row, so in a narrow
column — the employee Employment tab, the task dialog — it wrapped to two and
three lines and the clusters stopped reading as clusters. Roughly 1150px of row
becomes roughly 905px, by token arithmetic rather than measurement.
Where the width comes from, in order:
- Button side padding 0.5rem to 0.25rem. The old rule was scoped
`button[nbButton]`, which scores below Nebular's icon-only padding
(`.size-small.icon-start.icon-end.appearance-ghost` — six class-level
components, applied to any button whose only child is an `nb-icon`). So the
0.5rem reached the Font Awesome buttons and never the `nb-icon` ones, and the
two halves of the toolbar have been sitting on different paddings. Naming
Nebular's own hooks scores one above its highest rule and covers both.
- One icon box for both families. `nb-icon` was getting Nebular's 1rem
`button-small-icon-size` while the Font Awesome glyphs were set to 0.8125rem
here — and `<i>` being inline, each was as wide as its own glyph, so no two
buttons agreed on a width. At 0.875rem every button is 24px, which is the
WCAG 2.2 (2.5.8) minimum target size; the vertical padding is untouched, so
the ~30px height is unchanged.
- Divider side margins halved. Eleven of them cost as much row width as three
buttons.
The two selects are about matching the row rather than width — they were only
about a fifth of it. `input-appearance()` is included by page after page (this
one passes 42px) and reaches in through `::ng-deep` with `!important` to give
every `nb-select` beneath it a form-field height and background, which is why
these sat 12px taller than the buttons beside them, in a different colour, on
that page and nowhere else. Answering an `!important` needs a higher-scoring
selector, hence `:host`.
Nebular also renders an UNFILLED select a type step larger than a filled one
(`select-small-placeholder-text-font-size` 0.9375rem against the value's
0.8125rem), so "Font Family" was bigger than every other word in the row until
something was chosen. Both are 0.75rem now, filled or not.
Two deliberate changes beyond the reported problem: the select paints
`background-basic-color-1` against the toolbar band on every page, so it reads
as a control rather than blending into the bar; and the link/image popover
inputs get the same height treatment, since `input-appearance()` was giving them
the page's 42px too.
Shared component: this reaches every ga-rich-text-editor — the task dialog,
project and module mutation, the proposal editors, the public-page mutations.
* fix(location-form): drop the stray character beside the search icon
The `nb-icon` for the address autocomplete carried a literal "å" as its element
content, so it rendered as a stray character next to the search glyph in every
form that shows the autocomplete — organizations, contacts, candidates and the
employee Location tab. Self-closed instead; the icon is drawn from the `icon`
input, and the element was never meant to have content.
* feat(employee-location): rebuild the tab as two panels
The layout was `nb-card > nb-card-body.m-3 > .container > .row > .col-4 + .col-8`
— three levels of chrome, inside the tab's own card, around what is really one
form and one map. The two are views of the same value: type an address and the
pin follows, click the map and the fields follow. They read better as a pair of
equals than as a form with an illustration beside it, so they are now two
panels on the tab surface, each with its own fill, hairline and radius.
Three things move with the structure:
- The commit action was under BOTH columns, which put it under the map as much
as under the form and left it reading as a page-level action. It sits in a
footer on the panel whose fields it commits. It also takes `type="button"`:
inside a `<form>` with no `ngSubmit` binding it was defaulting to submit, and
only Angular's own submit handling kept that from doing anything.
- The fields are one column at one width. `ga-location-form` lays itself out on
a bootstrap grid — `col-11` for search, `col-8` for country and city, `col-6`
for postcode — widths chosen for a form running the width of a page. In a
22rem panel they read as four arbitrary field lengths and the postcode lands
around 150px. Latitude and longitude keep a shared row, being one value read
in two halves. Done from the consumer rather than in
`location-form.component.scss`, which is shared far more widely and whose
other hosts still want their own grid.
- A rule separates the autocomplete search from the fields below it. It FILLS
them rather than being one of them, and had nothing but the same 1rem gap to
say so.
The panel width is `min(22rem, 40%)` rather than a flat basis, following the
pattern already used elsewhere here: the layout column loses up to 860px to the
expanded AI chat, which no viewport media query can see because the window did
not change.
`ga-employee-location` has exactly two consumers — this tab and the candidate
Location tab — so the candidate page gets the same treatment. Same UI, same
purpose.
* fix(employee-location): size the map from its panel, not from the viewport
`.leaflet-container` is styled in `forms/maps/leaflet.component.scss` as a bare
`::ng-deep` with no `:host` in front of it, so Angular strips the scoping
attribute and the rule lands GLOBALLY — on every map in the app — carrying
`height: calc(100vh - 30rem) !important`. That is a guess at the chrome of
whichever page a map happens to be on, and it has nothing to do with the panel
the map now sits in: it left the map either short of the panel or running past
it, with the panel fill showing through the difference.
Flex the whole chain instead — `ga-leaflet-map`, the `.row` it wraps its map div
in, and the `.col-12` inside that — so the map is exactly as tall as its panel
and no arithmetic is involved. The `mt-3 mb-3` on that row is unset for the same
reason: inside a panel the spacing is the panel's to give, and a margin there
shows as a band of fill above and below the tiles.
`!important` answers an `!important`: the rule being overridden is global and
carries one, so nothing quieter reaches it. The height is set to `auto` rather
than to a figure of our own, because flex-grow is what sizes it.
The panel keeps a `min-height` floor. Leaflet measures its container when it
initialises, so there has to be something to measure, and the floor also keeps
the map from collapsing to a strip on a short viewport. Left alone: the
component has no resize handling of its own, but Leaflet's `trackResize` covers
window resizes and a container resize with no window resize behind it (the AI
chat opening) was already unhandled before this.
* fix(employees): drop the location tab's patches into the old scaffolding
The wrapper carried `::ng-deep` rules for `.container`, `.col-4` and an
`nb-card` — the markup `ga-employee-location` used to be built from. That
scaffolding is gone and the component owns its own layout now, so the rules name
nothing and can go with it.
What stays is the wrapper's actual job: the tab fill, and the flex handoff that
gives the component the height the tabset hands this tab. The fill is
load-bearing rather than decorative — the component paints its two panels but
not the gap between and around them, so without it the card body shows straight
through.
* fix(country-select): keep the dropdown panel within its field
The global `max-content` widening in _overrides.scss exists for a trigger too NARROW to show its options — a filter cell in a table row. A form field is the opposite case, and country names run to "South Georgia and the South Sandwich Islands", so the panel opened far wider than the field and over whatever sat beside it.
Opts out by class rather than by name: `.form-field-select` is the category — a select standing in a form column — so new ones add the class instead of growing the `:not()` list. ng-select copies the host's static class attribute onto the panel it appends to <body>, which is the only handle a stylesheet has on it.
Also gives the host `display: block`, so the field can be sized and aligned from outside like every other `div.form-group` in the address form, and drops a dead `size="medium"` attribute that NgSelectComponent declares no input for.
* feat(styles): add the employee tabset's shared panel language
Every tab of the employee edit page invented its own chrome. Some wrapped their form in an `nb-card.m-3` inside the tab host — a card inside a card inside the page card. Some borrowed the ORGANISATION page's stylesheet, which brought a 563px panel and a w-25/w-75 split written for a different page. Each laid its fields out on whatever bootstrap columns were to hand.
The location tab was rebuilt as flat panels on the tab surface and the account tab followed it. This is that shape factored out — `employee-tab-host` for the surface and flex chain, `employee-tab-panel` for the panel, field grid and footer — so the remaining tabs do not each re-derive it and the rhythm can be moved from one file.
* fix(employee-account): rebuild the account tab as two panels
The tab was rendering the ORGANISATION page's scaffolding, borrowed through styleUrls: a w-25 column holding nothing but an avatar pinned to its top-left corner, beside a w-75 column of bootstrap rows. At an employee's width that left the left quarter empty from the photo down and stretched every input across half the card. The tab's own stylesheet was spending itself overriding that import selector for selector, winning only on source order; the import is gone and the tab owns its layout.
Identity panel: photo on the leading edge with the name, position, blurb and status chips beside it, then the record's figures as one flat two-column grid. The groups are `display: contents` so the individual rows are the grid items — laid out as blocks, a two-row group beside a three-row group was padded to match and left a hole under the shorter one. Type runs at 11-12px so the whole panel fits without scrolling the tab.
Form panel: six fields on a two-column grid, replacing three bootstrap rows whose columns were declared three different ways, with Save in a footer on the panel's trailing edge.
The avatar guard now watches for a load error too: guarding on the URL alone only proved one was SET, so an employee whose stored photo 404s got the browser's broken-image glyph. The inline 68px SVG hard-coded to #0091FF is an nb-icon camera badge, and the vetted nb-badge — which positions itself absolutely and needed an inline `position: relative` to sit in the flow — is a plain chip. NbBadgeModule drops out of the module with it.
* fix(employee-networks): put the eight profile URLs on one grid
They were four bootstrap rows of two `col-sm-3` cells: each field took a QUARTER of the row and the right half of every row was empty, so the form ran down the left side of a card twice as wide as anything in it.
One panel on the tab surface, eight fields on the tabset's shared two-column grid, Save in the panel footer. The organisation page's stylesheet drops out of styleUrls with the layout it was propping up.
* fix(employee-hiring): put the three dates on the tab's grid
Two bootstrap rows, the second carrying an empty `<div class="col"></div>` to hold the reject date at half width — a spacer standing in for a layout rule. The grid places the third date under the first without one.
Same panel and footer as the sibling tabs; the organisation page's stylesheet drops out of styleUrls.
* fix(employee-employment): drop the nested cards and grid the fields
`nb-card > nb-card-body > .form-container > form` was a card inside a card inside the page card — three levels of frame around one form. It is one panel on the tab surface now.
The fields ran down three different sets of bootstrap columns (col-md-6, then bare col, then col-md-6), so the same form changed its column width twice on the way down. They are on the tabset's two-column grid, with the description and the bonus toggle spanning it.
The start-date note was its own column BESIDE the field it is about, so the amber block sat where the next field belonged and shifted everything below it; it sits under its own field now. The start-date input also gains the id its label's `for` was already pointing at.
* fix(employee-rates): rebuild the rate and limits cards as two panels
Two `nb-card`s in bootstrap columns inside `.content > .employee-form > form` inside the tab host. The nesting also cost the fields their alignment: inside a `col`, a `col-6` is a QUARTER of the row, so the four rate inputs sat at quarter width beside a limits card whose one input ran the full width of its own.
Two panels on the tab surface, 3:2, each with a two-column grid, and Save in a footer on the rate panel it commits rather than under both columns. The card headers are kept as captions — a card header brings its own fill, border and padding on every side, which two words naming a group of fields do not need.
The validation messages were siblings of the .form-group rather than children, so each rendered under the NEXT field down; they sit inside their own field now.
ga-employee-rates is shared with the candidate page, so that tab gains the surface fill and flex chain the new panels need — without it the gutter between them would show its card body through. Its own `max-height` chrome arithmetic is left alone.
* fix(employee-membership): make the assigned entities rows, not cards
Every entity in the list was wrapped in an `nb-card` of its own — fill, border, radius and 1rem of body padding for one line of text and a close icon — inside a card, inside the tab's card. A name with a remove button beside it is a row.
One panel: a header naming the list with Add on its trailing edge, then the entities as rows. The title moved out of the scrolling body, where the one label saying whether you were looking at contacts or projects scrolled away with the rows it named. The add form was `.container.m-0.w-25` — a quarter of the card whatever its width — so the select was narrower than the rows it adds to; it takes the panel's width now.
The remove control was an unlabelled `nb-action` from a toolbar construct, floated right. It is a labelled ghost button, muted at rest and full strength on hover or keyboard focus — deliberately never hidden, since a hover-only affordance is unreachable on a touch screen.
NbCardModule and NbActionsModule drop out; NbTooltipModule comes in for the label. The Contacts and Projects tab shells lose the nb-card overrides that now have nothing to override.
* fix(employee-settings): align the tab and drop the organisation page's dead rules
The fields were laid out on four different sets of bootstrap columns — `col-6` in two sections, `col-xl-6 col-12 pt-2` in the other two, and on one toggle the `form-group invite-toggle` classes were on the COLUMN itself rather than a div inside it, so that row sat on different padding from its five neighbours. Each section body is on one two-column grid now.
`width: calc(100% - 230px)` on the sections column was the rail's width plus a guess at the gap, restated as a percentage — moving the rail meant remembering to move this too. Flex and `min-width: 0` say it directly. Save moves to a footer on the trailing edge, above a rule, like every other tab.
The rail rows were `<ul><span (click)><li>` — the click target was the element BETWEEN the list and its item, which is neither valid list content nor focusable. They are buttons, reachable by keyboard, and the open one reads as a current item rather than as the one dented button among four raised ones.
Fixes two real bugs found on the way: the six info-tooltip buttons had no `type`, so inside the form they defaulted to `type="submit"` and clicking one submitted the settings form; and the Time Format label was `for="timeZone"`, pointing at the select in the field beside it, so clicking it opened the wrong dropdown.
Removes ~150 lines copied from the organisation settings page that name nothing in this template: nth-of-type(5/6/7), .fiscal-years, .half-width-inputs, .design-select, .color-pick, .year-pick, .time-format-select.
* fix(employees): rebuild the edit page header as one identity row
The header was a flex-column of two rows, the second inset by `$meta-indent: calc(3rem + 48px + 0.75rem)` — the summed width of the back button, the avatar and a gap, hand-computed so the meta would line up under the name. That indent is why the public-page button and the email appeared to float in the middle of the header with nothing above or below to align to, and it had to be re-derived by hand every time the avatar changed size. On one row nothing needs measuring.
Position, username and email are one muted supporting line with the separator drawn between items, so a missing position cannot leave a leading dot behind. The em-dash the position used to fall back to — which read as a stray character under the name — is gone with it.
The avatar guard now watches for a load error: guarding on the URL alone only proved one was SET, so an employee whose stored photo 404s got the browser's broken-image glyph in the page header. It falls back to the same placeholder an employee with no photo gets.
Both actions move to the trailing edge. Edit Public Page was a `<span (click)>` — not focusable — and is a button; the favourite toggle renders a FILLED nbButton, which on the dark theme painted a solid near-white block in the corner of a near-black header for a secondary action, so it is quietened to match the pill beside it. Done from this page's stylesheet, not the shared component's, where a filled button is what the list rows want.
* chore(employees): drop the explanatory comments added in this branch
Removes the commentary this branch introduced across the employee edit
page, its tabs and the shared styles they lean on. Comments that predate
the branch are untouched, and two that the branch had reworded are back
to their original wording.
No behaviour or style change: stripping every comment from both revisions
leaves identical code.
* fix(employee-profile): weight the tab inputs from the paragraph token
Nebular's `[nbInput]` carries its own font-weight, which left the values
in the edit-employee tabs heavier than the body copy around them. Resolve
it from `text-paragraph-font-weight` so the fields read at the same weight
as the rest of the page on every theme.
* chore(cspell): add formgroupname and normalise indentation
Adds the formgroupname dictionary word used by the employee edit page
forms, and converts the space-indented entries at the top of the words
list to tabs to match the rest of the file.
* fix(employee-membership): step the card typography down a size
The membership card set its title and its member names at the paragraph
size, so the Contacts and Projects tabs read heavier than the surrounding
form. Resolve both from `text-subtitle-2-font-size`, drop the title to a
500 weight, and pin the names to `text-paragraph-font-weight` so they
cannot inherit a heavier weight from above.
* fix(employee-profile): address review findings on the tab layouts
- let .tab shrink so .tab__fields scrolls internally and .tab__actions
stays pinned at the panel bottom instead of scrolling out of view
- move the rates submit footer after the limits panel; .rates is a grid
now, so the footer spans both columns rather than becoming a third one
- render zero-valued employee facts (bill rate, weekly limit, work hours,
job success, total jobs) by testing for null rather than truthiness
- expose aria-expanded on the settings accordion rail buttons
- reuse employee-tab-host for the contact, projects, documents, rate and
location tab hosts, which each carried their own copy of it
- floor the candidate rates max-height so it cannot resolve to a
non-positive length, and collapse the panel, below a 28rem viewport
- merge the duplicate :host blocks in country.component.scss
* fix(employee-profile): patch the avatar control and share the tab form rule
- patch imageUrl and imageId as soon as an upload succeeds, instead of
waiting on the userForm$ -> user update -> refetch round-trip to bring
them back. imageId is an enabled control, so saving before the reload
landed posted the old asset id and reverted the avatar
- move .tab__form into @mixin employee-tab-panel; employment, hiring and
networks each carried an identical copy
- let ga-employee-location and ga-employee-rates shrink to the host
height, so their own field areas take the scroll and their footers stay
pinned rather than scrolling out of view
|
||
|
|
52ae8427fe |
Fix: make ESLint runnable again across the workspace (#10117)
No lint invocation in this repo could reach a single file. `nx run-many -t lint --all` failed for
93 of 93 projects, and no CI workflow runs lint, so nothing ever noticed. Two independent bugs,
either of which alone was fatal.
1. The root `eslint.config.js` was `module.exports = new FlatESLint({ overrides: [] })`, importing
a `FlatESLint` class from `@nx/eslint-plugin-nx`. That could never have worked: a flat config
must export an array, and no Nx package has ever exported such a class. `@nx/eslint-plugin-nx`
is the beta-only predecessor NAME of `@nx/eslint-plugin` — the repo declared both, the real one
at ^22.5.2 and this one pinned at 16.0.0-beta.1. The v16 beta drags a nested `@nx/devkit@16`
that expects an `nx` internal path nx@22 no longer ships, so every `eslint` invocation died
with `Cannot find module 'nx/src/utils/typescript'`.
2. All 41 project configs did `[...require('../../.eslintrc.json')]`. That file is a JSON object,
so the spread threw `TypeError: baseConfig is not iterable` — a different failure, on the path
`nx lint <project>` actually takes. `packages/mcp-server` also had the wrong depth.
Changes: remove `@nx/eslint-plugin-nx`, add `typescript-eslint@^8.40.0` (the only genuinely
missing package — `@nx/eslint-plugin`'s flat configs require it unconditionally); rewrite the root
config as a real flat array shaped like Nx 22's own generator output, with real `ignores`
replacing the legacy `"ignorePatterns": ["**/*"]` that disabled linting workspace-wide; repoint
all 41 project configs at the root flat config, dropping the FlatCompat bridge the 12 `.cjs` ones
used; disable the deprecated `@typescript-eslint/no-empty-interface`, which Nx's presets still
enable alongside its v8 replacement `no-empty-object-type` and so double-reported every
occurrence; and re-enable `no-dupe-keys`, `no-dupe-class-members`, `no-dupe-args` and
`no-unreachable`, which typescript-eslint's `eslint-recommended` overlay turns off on the grounds
that `tsc` reports them — no CI job here runs `tsc --noEmit`, and a duplicate key in
`packages/core/jest.config.ts` has already silently changed behaviour once.
The legacy `.eslintrc.json` files are deliberately KEPT: `.codacy/codacy.yaml` pins eslint@8.57.0,
which does read them.
The guardrail is proven, not assumed — against the real historical bug:
packages/core/jest.config.ts
29:2 error Duplicate key 'transformIgnorePatterns' no-dupe-keys
Findings are REPORTED, NOT FIXED (a separate job): 8,887 files linted, 2,290 with problems,
2,649 errors / 6,110 warnings, zero parse errors. Lint should not become a required check until
that backlog is triaged.
Verified: packages/core 56/56 suites and 612/612 tests; build-monorepo-root, build-api, build-libs
and build-web all green; all 42 configs load; `nx lint <project>` now runs and reports instead of
crashing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b5d375eac4 |
fix(employees): put the Time & Activity filters on the trailing edge (#10114)
* fix(employees): put the Time & Activity filters on the trailing edge The filter bar sat wherever its box happened to begin instead of flush right, on every tab of the Time & Activity page. `time-activities.component.scss` resets `.col-auto.ml-auto`, dropping the filter bar's own auto-margin push along with the negative gutters it went with. Nothing replaced it, so the header's `space-between` had no effect: `.header-filters` grew to fill the row and its contents started at the left of that space. End the row explicitly instead. `.header-filters` becomes a flex slot with `justify-content: flex-end`, and the projected `.main-wrapper` is given full width and ends its own contents. Screenshots and Apps / Visited Sites forward this file, so all three tabs are covered; the Screenshots stylesheet redeclares the same `.header-bar` block, so it restates the slot rule that its copy overrides. The Videos tab lives in `@gauzy/plugin-videos-ui` and forwards nothing, so it carries an equivalent block of its own, plus the inset that lines its controls up with the video grid below. `gauzy-filters.component.scss` is left alone. It is shared with the timesheet and recap surfaces, where the current alignment is wanted. * fix(report): drop the project column heading to 12px The project name in the Project column was set at 14px with 17px leading, a step above the 12px table-header scale the report tables around it run at. In a grid whose cells are all body text, it read as a heading rather than as the first line of a cell. Bring it onto the same scale: 12px with 15px leading, the values behind `gauzy-table-header-font-size` and `gauzy-table-header-line-height`. `.employees-count` beneath it keeps its 11px; it is the secondary line of the cell and still sits a step below the name. The recap copy of this component in `@gauzy/desktop-ui-lib` is a separate surface and is left at the old scale. * fix(employees): anchor the Time & Activity grid on the table-header scale The typography cap for the grid was written as a list of overrides hung off `ga-daily-grid`, which reached the chrome from `report.scss` but not the nested cell components. Each of those ships its own px size, so the Employee, Project and Client cells kept their own scale and the rows were set at three different sizes. Move the declaration onto `.grid-container` so it inherits down through everything that carries no size of its own, and list beneath it only the elements that do declare one: the `report.scss` chrome, `ngx-avatar`, `ga-project-column-view` and `ngx-contact-links`. Every size resolves to `gauzy-table-header-font-size` (0.75rem) rather than a literal, so the grid tracks the same scale as the group-by label and the stat-strip labels capped above it. Line-height is restated wherever the size is, because these components pair a px size with a px leading, and changing the size alone would leave leading set for larger text. The 8px counter chip in the client cell is left alone: it is a count badge rather than body text. Screenshots and Apps / Visited Sites forward this file, so all three tabs move together. * fix(report): repeat both To Do columns in the daily grid header The accordion header stood a single `todo-column header` spacer in place of the two 30% columns the data rows carry, To Do and Notes. That left the header summing to 118% against the rows' 150% — and because both overflow their container, flexbox shrank each by its own ratio, so every column boundary past To Do resolved somewhere different. Time and Activity in the trigger sat left of the columns they head. Repeat the same pair of spacers the rows use, in all four grouping branches. Both rows now sum to 150% and shrink alike, so the header's figures line up with the table beneath them. * fix(report): inset the daily grid header's trailing edge Two things still separated the accordion header from the rows once both carried the same columns. The rows resolve inside `nb-accordion-item-body` and its inset, while the header resolves in the card; and the header carries the expand chevron as an extra flex item after the last column. Both leave its columns ending further along than the rows', so Time and Activity read as pushed right. Inset the header's trailing edge by the chevron's width to bring them back onto the columns the table below uses, mirrored for RTL. `.todo-column.header` and its `.client` variant go with it. Those widths existed only to approximate the column the header was missing, and nothing references them now that it carries both. * fix(employees): let Time & Activity rows size to their content `report.scss` gives every row a fixed 86px box, pads it 10px top and 20px bottom and pins the content to the top. That was sized for the old type scale; at the table-header size the row's single `.table-inner-wrapper` fills a little over half of it and the remainder reads as a gap beneath each row. Let the content set the height and pad it evenly, which brings a row down to roughly 56px and puts more of the report on screen. Scoped to this page rather than changed in `report.scss`, which shares `.table-row` with the payment, expenses and amounts-owed grids; those run at their own scale and are left as they are. * fix(report): reduce the project column logo to 24px The project logo sat at 28px, sized against the 14px project name it used to stand beside. With the name now at the 12px table-header scale it read heavy for the cell. Bring it down a step to 24px. The `min-width` and `min-height` come with it — they pin the box against the flex row and would otherwise hold it at 28px. The image inside is sized at 100% and follows on its own. * fix(employees): line the Time & Activity filters up with the table The filter row ended past the table's trailing edge, and by a different amount depending on whether the table was long enough to scroll. The scroll container was padded rather than narrowed. A scrollbar draws on its element's own trailing edge, outside the padding, so `padding-right` moved the table's text inward while leaving the bar itself against the card's edge — and the row above, which has no scrollbar to account for, had nothing to line up with. Hold the container 11px off the edge instead. The bar travels in with it and the 5px it occupies puts the table's trailing edge at a 16px inset, which is where the filter row already ends: 8px on the header bar plus the 8px its own wrapper adds. * fix(employees): pull the Time & Activity filter row off the card's edge The filter row still ran past the table's trailing edge. `.filter-item-list` is a bootstrap `.row` and the page resets the negative gutters on `.main-wrapper` but never on that inner row, so its trailing item overhangs the wrapper it sits in and the row ends wider than the table below it. Carry that difference on the header bar. Fixing it at the source would mean resetting the gutters inside `ngx-gauzy-filters`, which the timesheet and recap surfaces also use and which the filter work on this branch has so far left alone. The inset is set against the rendered result rather than derived, since the overhang comes from gutters this page does not own. * fix(report): tune the daily grid header's trailing inset Brings the trigger's Time and Activity closer to the columns they head. The 44px this replaces was an estimate, sized to the chevron on the assumption that it was the only thing separating the header from the rows. It is not: the rows also resolve inside `nb-accordion-item-body` and its inset, so no single figure follows from the stylesheet. The comment now says as much, and the value is set against the rendered result. * fix(employees): tune the Time & Activity filter row's trailing inset Moves the filter row 4px towards the card's trailing edge. Like the inset it adjusts, the value is set against the rendered result: the overhang comes from bootstrap gutters inside `ngx-gauzy-filters` that this page does not own and deliberately does not reset. |