Skip to content

Commit c33e2bc

Browse files
committed
Merge remote-tracking branch 'upstream/main' into wildan/86116-fix-duplicate-workspace-distance-custom-unit
2 parents 0bf8c5e + 3031061 commit c33e2bc

137 files changed

Lines changed: 886 additions & 767 deletions

File tree

Some content is hidden

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

config/eslint/eslint.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ const config = defineConfig([
269269
'@typescript-eslint/max-params': ['error', {max: 10}],
270270
'@typescript-eslint/naming-convention': [
271271
'error',
272+
{
273+
selector: ['variable', 'property'],
274+
format: null,
275+
// Allow __esModule because it is a well-known interop property injected by bundlers
276+
// (e.g. Babel/Webpack) and sometimes required by library internals (e.g. react-native-skia).
277+
filter: {
278+
regex: '^__esModule$',
279+
match: true,
280+
},
281+
},
272282
{
273283
selector: ['variable', 'property'],
274284
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],

config/eslint/eslint.seatbelt.tsv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
"../../src/components/RadioButtonWithLabel.tsx" "no-restricted-syntax" 1
117117
"../../src/components/Reactions/AddReactionBubble.tsx" "react-hooks/refs" 1
118118
"../../src/components/Reactions/MiniQuickEmojiReactions.tsx" "react-hooks/refs" 1
119-
"../../src/components/ReimbursementAccountLoadingIndicator.tsx" "@typescript-eslint/no-deprecated" 1
120119
"../../src/components/ReportActionItem/MoneyRequestReceiptView.tsx" "react-hooks/set-state-in-effect" 1
121120
"../../src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx" "react-hooks/preserve-manual-memoization" 2
122121
"../../src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx" "react-hooks/refs" 1

contributingGuides/NAVIGATION.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,11 +750,49 @@ KEYBOARD_SHORTCUTS: {
750750
> and makes it harder to reason about which screens can trigger a given flow.
751751
> When in doubt, prefer an explicit list.
752752

753-
### Current limitations (work in progress)
753+
### Optional path parameters
754754

755-
- **Optional path parameters:** Suffixes must not include optional path params (e.g. `a/:reportID?`). Required path parameters (e.g. `flag/:reportActionID`) and query parameters are supported - see [Dynamic routes with query parameters](#dynamic-routes-with-query-parameters).
755+
Dynamic route suffixes may declare optional path parameters by appending `?` to the parameter name.
756+
An optional parameter can be present or absent in the URL; when absent, the corresponding key is
757+
omitted from the navigation state's `params` (it is **not** set to `undefined`).
756758

757-
If you try to use dynamic routes with optional path parameters now, you will either fail to navigate to the page at all or end up on a non-existent page, and the navigation will be broken.
759+
Optionals can appear anywhere in the suffix:
760+
761+
- **Trailing optional:** `member-details/:accountID?` matches both `member-details` and `member-details/123`.
762+
- **Middle optional:** `wrap/:p?/end` matches both `wrap/end` and `wrap/x/end`.
763+
- **Multiple optionals:** `a/:p1?/b/:p2?` matches `a/b`, `a/x/b`, `a/b/y`, and `a/x/b/y`.
764+
765+
#### Configuration example
766+
767+
```ts
768+
DYNAMIC_ROUTES: {
769+
MEMBER_DETAILS: {
770+
path: 'member-details/:accountID?',
771+
entryScreens: [SCREENS.WORKSPACE.MEMBERS],
772+
getRoute: (accountID?: string) => (accountID ? `member-details/${accountID}` : 'member-details'),
773+
},
774+
},
775+
```
776+
777+
#### Precedence rules
778+
779+
When several registered suffixes could match the same URL, the matcher uses a deterministic
780+
three-phase order. Each phase exhausts all sub-suffix lengths (longest to shortest) before the
781+
next phase begins:
782+
783+
1. **Static beats everything.** All sub-suffix lengths are checked for an exact static match
784+
first. A short static match (e.g. single-segment `country`) always beats a longer parametric
785+
match (e.g. `:reportID/country`).
786+
2. **Strict parametric beats optional parametric.** After statics, all sub-suffix lengths are
787+
checked for strict parametric patterns (no optional params, e.g. `flag/:reportID/:reportActionID`).
788+
Only after those are exhausted does the matcher try optional parametric patterns
789+
(e.g. `page/:id?`).
790+
3. **Longest match wins within each phase.** Within a single phase the algorithm iterates from
791+
the longest candidate to the shortest, so a 3-segment match beats a 2-segment one when both
792+
are registered.
793+
4. **Among patterns of the same kind: first registered wins.** If multiple patterns of the same
794+
type (strict or optional) could match the same candidate, the one declared first in
795+
`DYNAMIC_ROUTES` wins.
758796

759797
### Multi-segment dynamic routes
760798

jest/setup.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ jest.mock('expo-location', () => ({
7676

7777
// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest
7878
jest.mock('react-native/Libraries/LogBox/LogBox', () => ({
79-
// eslint-disable-next-line @typescript-eslint/naming-convention
8079
__esModule: true,
8180
default: {
8281
ignoreLogs: jest.fn(),
@@ -149,7 +148,6 @@ jest.mock('@libs/scheduleOnLiveMarkdownRuntime', () => {
149148
});
150149

151150
jest.mock('@src/setup/telemetry', () => ({
152-
// eslint-disable-next-line @typescript-eslint/naming-convention
153151
__esModule: true,
154152
default: jest.fn(),
155153
navigationIntegration: {
@@ -280,7 +278,6 @@ jest.mock(
280278
);
281279

282280
jest.mock('@libs/prepareRequestPayload/index.native.ts', () => ({
283-
// eslint-disable-next-line @typescript-eslint/naming-convention
284281
__esModule: true,
285282
default: jest.fn((command: string, data: Record<string, unknown>) => {
286283
const formData = new FormData();
@@ -303,7 +300,6 @@ jest.mock('@libs/prepareRequestPayload/index.native.ts', () => ({
303300
jest.mock('@components/ConfirmedRoute.tsx');
304301

305302
jest.mock('@src/hooks/useWorkletStateMachine/runOnUISync', () => ({
306-
// eslint-disable-next-line @typescript-eslint/naming-convention
307303
__esModule: true,
308304
default: jest.fn(() => jest.fn()), // Return a function that returns a function
309305
}));
@@ -367,17 +363,14 @@ jest.mock('@src/components/KeyboardDismissibleFlatList/KeyboardDismissibleFlatLi
367363
// in triggerUnreadUpdate (also timer-based), this creates excessive timer churn that causes
368364
// heavy integration tests like SessionTest to exceed their timeout.
369365
jest.mock('@src/hooks/useDocumentTitle', () => ({
370-
// eslint-disable-next-line @typescript-eslint/naming-convention
371366
__esModule: true,
372367
default: jest.fn(),
373368
}));
374369
jest.mock('@src/hooks/useWorkspaceDocumentTitle', () => ({
375-
// eslint-disable-next-line @typescript-eslint/naming-convention
376370
__esModule: true,
377371
default: jest.fn(),
378372
}));
379373
jest.mock('@src/hooks/useDomainDocumentTitle', () => ({
380-
// eslint-disable-next-line @typescript-eslint/naming-convention
381374
__esModule: true,
382375
default: jest.fn(),
383376
}));

src/ROUTES.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,14 @@ const DYNAMIC_ROUTES = {
304304
path: 'imported',
305305
entryScreens: [SCREENS.WORKSPACE.CATEGORIES],
306306
},
307+
WORKSPACE_CATEGORIES_SETTINGS: {
308+
path: 'categories-settings',
309+
entryScreens: [SCREENS.WORKSPACE.CATEGORIES],
310+
},
311+
WORKSPACE_CATEGORY_CREATE: {
312+
path: 'category-new',
313+
entryScreens: [SCREENS.WORKSPACE.CATEGORIES],
314+
},
307315
SPEND_CATEGORY_SELECTOR: {
308316
path: 'spend-category-selector/:groupID',
309317
entryScreens: [SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_SETTINGS],
@@ -2361,14 +2369,6 @@ const ROUTES = {
23612369
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
23622370
getRoute: (backTo?: string) => getUrlWithBackToParam(`workspaces/pay-and-downgrade` as const, backTo),
23632371
},
2364-
WORKSPACE_CATEGORIES_SETTINGS: {
2365-
route: 'workspaces/:policyID/categories/settings',
2366-
getRoute: (policyID: string) => `workspaces/${policyID}/categories/settings` as const,
2367-
},
2368-
WORKSPACE_CATEGORY_CREATE: {
2369-
route: 'workspaces/:policyID/categories/new',
2370-
getRoute: (policyID: string) => `workspaces/${policyID}/categories/new` as const,
2371-
},
23722372
WORKSPACE_CATEGORY_EDIT: {
23732373
route: 'workspaces/:policyID/category/:categoryName/edit',
23742374
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/edit` as const,

src/SCREENS.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ const SCREENS = {
777777
NAME: 'Workspace_Overview_Name',
778778
CLIENT_ID: 'Workspace_Overview_Client_ID',
779779
CATEGORY_CREATE: 'Category_Create',
780+
DYNAMIC_CATEGORY_CREATE: 'Dynamic_Category_Create',
780781
CATEGORY_EDIT: 'Category_Edit',
781782
CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code',
782783
CATEGORY_GL_CODE: 'Category_GL_Code',
@@ -789,6 +790,7 @@ const SCREENS = {
789790
CATEGORY_REQUIRE_ITEMIZED_RECEIPTS_OVER: 'Category_Require_Itemized_Receipts_Over',
790791
CATEGORY_REQUIRED_FIELDS: 'Category_Required_Fields',
791792
CATEGORIES_SETTINGS: 'Categories_Settings',
793+
DYNAMIC_CATEGORIES_SETTINGS: 'Dynamic_Categories_Settings',
792794
DYNAMIC_CATEGORIES_IMPORT: 'Dynamic_Categories_Import',
793795
DYNAMIC_CATEGORIES_IMPORTED: 'Dynamic_Categories_Imported',
794796
DYNAMIC_SPEND_CATEGORY_SELECTOR: 'Dynamic_Spend_Category_Selector',

src/components/Charts/BarChart/BarChartContent.tsx

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import ChartTooltipLayer from '@components/Charts/components/ChartTooltipLayer';
1010
import ChartXAxisLabels from '@components/Charts/components/ChartXAxisLabels';
1111
import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels';
1212
import {AXIS_LABEL_GAP, CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, GLYPH_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
13-
import type {ComputeGeometryFn, HitTestArgs} from '@components/Charts/hooks';
13+
import type {HitTestArgs} from '@components/Charts/hooks';
1414
import {
1515
useChartFontManager,
1616
useChartInteractions,
@@ -22,7 +22,7 @@ import {
2222
useYAxisLabelWidth,
2323
} from '@components/Charts/hooks';
2424
import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
25-
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getAdditionalOffset, getChartColor, rotatedLabelYOffset} from '@components/Charts/utils';
25+
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
2626
import useTheme from '@hooks/useTheme';
2727
import useThemeStyles from '@hooks/useThemeStyles';
2828
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
@@ -36,29 +36,6 @@ const BAR_INNER_PADDING = 0.3;
3636
*/
3737
const BASE_DOMAIN_PADDING = {top: 32, bottom: 1, left: 0, right: 0};
3838

39-
/**
40-
* Bar chart geometry for label hit-testing.
41-
* Labels are center-anchored: the 45° parallelogram's upper-right corner is offset
42-
* by (halfLabelWidth * sinA) right and up, so the box straddles the tick symmetrically.
43-
*/
44-
const computeBarLabelGeometry: ComputeGeometryFn = ({ascent, descent, sinA, angleRad, labelWidths, padding}) => {
45-
const maxLabelWidth = labelWidths.length > 0 ? Math.max(...labelWidths) : 0;
46-
const centeredUpwardOffset = angleRad > 0 ? (maxLabelWidth / 2) * sinA : 0;
47-
const halfLabelSins = labelWidths.map((w) => (w / 2) * sinA - variables.iconSizeExtraSmall / 3);
48-
const halfWidths = labelWidths.map((w) => w / 2);
49-
const additionalOffset = getAdditionalOffset(angleRad);
50-
return {
51-
labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) + centeredUpwardOffset - additionalOffset,
52-
iconSin: variables.iconSizeExtraSmall * sinA,
53-
labelSins: labelWidths.map((w) => w * sinA),
54-
halfWidths,
55-
cornerAnchorDX: halfLabelSins,
56-
cornerAnchorDY: halfLabelSins.map((v) => -v),
57-
yMin90Offsets: halfWidths.map((hw) => -hw + padding),
58-
yMax90Offsets: halfWidths.map((hw) => hw + padding),
59-
};
60-
};
61-
6239
type BarChartProps = CartesianChartProps & {
6340
/** Callback when a bar is pressed */
6441
onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -141,14 +118,12 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
141118
labelRotation,
142119
labelSkipInterval,
143120
chartBottom,
144-
computeGeometry: computeBarLabelGeometry,
145121
});
146122

147123
const handleChartBoundsChange = (bounds: ChartBounds) => {
148124
const domainWidth = bounds.right - bounds.left;
149125
const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * domainWidth) / data.length;
150126
barWidth.set(calculatedBarWidth);
151-
chartBottom.set(bounds.bottom);
152127
yZero.set(0);
153128
setBarAreaWidth(domainWidth);
154129
setBoundsLeft(bounds.left);
@@ -216,6 +191,10 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
216191
if (!fontMgr || xAxisLabelHeight === undefined) {
217192
return null;
218193
}
194+
195+
const chartBoundsBottom = args.yScale(Math.min(...args.yTicks));
196+
chartBottom.set(chartBoundsBottom);
197+
219198
return (
220199
<>
221200
<ChartXAxisLabels
@@ -231,8 +210,7 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
231210
fontMgr={fontMgr}
232211
labelColor={theme.textSupporting}
233212
xScale={args.xScale}
234-
chartBoundsBottom={args.chartBounds.bottom}
235-
centerRotatedLabels
213+
chartBoundsBottom={chartBoundsBottom}
236214
/>
237215
<ChartYAxisLabels
238216
yTicks={args.yTicks}
@@ -258,7 +236,7 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
258236
fontMgr,
259237
variables.iconSizeExtraSmall,
260238
);
261-
const chartPadding = {...CHART_PADDING, bottom: labelSpace + CHART_PADDING.bottom + variables.iconSizeExtraSmall, left: yAxisLabelWidth + GLYPH_PADDING};
239+
const chartPadding = {...CHART_PADDING, bottom: labelSpace + CHART_PADDING.bottom, left: yAxisLabelWidth + GLYPH_PADDING};
262240

263241
if (isLoading || !fontMgr) {
264242
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'BarChartContent', isLoading, isFontLoading: !fontMgr};

src/components/Charts/LineChart/LineChartContent.tsx

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels';
1212
import LeftFrameLine from '@components/Charts/components/LeftFrameLine';
1313
import ScatterPoints from '@components/Charts/components/ScatterPoints';
1414
import {AXIS_LABEL_GAP, CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, GLYPH_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
15-
import type {ComputeGeometryFn, HitTestArgs} from '@components/Charts/hooks';
15+
import type {HitTestArgs} from '@components/Charts/hooks';
1616
import {
1717
useChartFontManager,
1818
useChartInteractions,
@@ -24,7 +24,7 @@ import {
2424
useYAxisLabelWidth,
2525
} from '@components/Charts/hooks';
2626
import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
27-
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getAdditionalOffset, rotatedLabelYOffset} from '@components/Charts/utils';
27+
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR} from '@components/Charts/utils';
2828
import useTheme from '@hooks/useTheme';
2929
import useThemeStyles from '@hooks/useThemeStyles';
3030
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
@@ -42,26 +42,6 @@ const MIN_SAFE_PADDING = DOT_RADIUS + DOT_HOVER_EXTRA_RADIUS;
4242
/** Base domain padding applied to all sides */
4343
const BASE_DOMAIN_PADDING = {top: 16, bottom: 16, left: 0, right: 0};
4444

45-
/**
46-
* Line chart geometry for label hit-testing.
47-
* Labels are start-anchored at the tick: the 45° parallelogram's upper-right corner is
48-
* offset by (iconSize/3 * sinA) left and down, placing the box just below the axis line.
49-
*/
50-
const computeLineLabelGeometry: ComputeGeometryFn = ({ascent, descent, sinA, angleRad, labelWidths, padding}) => {
51-
const iconThirdSin = (variables.iconSizeExtraSmall / 3) * sinA;
52-
const additionalOffset = getAdditionalOffset(angleRad);
53-
return {
54-
labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) - additionalOffset,
55-
iconSin: variables.iconSizeExtraSmall * sinA,
56-
labelSins: labelWidths.map((w) => w * sinA),
57-
halfWidths: labelWidths.map((w) => w / 2),
58-
cornerAnchorDX: labelWidths.map(() => -iconThirdSin),
59-
cornerAnchorDY: labelWidths.map(() => iconThirdSin),
60-
yMin90Offsets: labelWidths.map(() => padding),
61-
yMax90Offsets: labelWidths.map((w) => w + padding),
62-
};
63-
};
64-
6545
type LineChartProps = CartesianChartProps & {
6646
/** Callback when a data point is pressed */
6747
onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -139,7 +119,6 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
139119
labelAreaWidth: plotAreaWidth,
140120
firstTickLeftSpace: boundsLeft + domainPadding.left * paddingScale,
141121
lastTickRightSpace: chartWidth > 0 ? chartWidth - boundsRight + domainPadding.right * paddingScale : 0,
142-
allowTightDiagonalPacking: true,
143122
measurements,
144123
});
145124

@@ -158,14 +137,12 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
158137
labelRotation,
159138
labelSkipInterval,
160139
chartBottom,
161-
computeGeometry: computeLineLabelGeometry,
162140
});
163141

164142
const handleChartBoundsChange = (bounds: ChartBounds) => {
165143
setPlotAreaWidth(bounds.right - bounds.left);
166144
setBoundsLeft(bounds.left);
167145
setBoundsRight(bounds.right);
168-
chartBottom.set(bounds.bottom);
169146
};
170147

171148
const checkIsOverDot = (args: HitTestArgs) => {
@@ -197,6 +174,8 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
197174
}));
198175

199176
const renderOutside = (args: CartesianChartRenderArg<{x: number; y: number}, 'y'>) => {
177+
const chartBoundsBottom = args.yScale(Math.min(...args.yTicks));
178+
chartBottom.set(chartBoundsBottom);
200179
return (
201180
<>
202181
<LeftFrameLine
@@ -224,7 +203,7 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
224203
fontMgr={fontMgr}
225204
labelColor={theme.textSupporting}
226205
xScale={args.xScale}
227-
chartBoundsBottom={args.chartBounds.bottom}
206+
chartBoundsBottom={chartBoundsBottom}
228207
/>
229208
)}
230209
{!!fontMgr && (
@@ -253,7 +232,7 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
253232
fontMgr,
254233
variables.iconSizeExtraSmall,
255234
);
256-
const chartPadding = {...CHART_PADDING, bottom: labelSpace + CHART_PADDING.bottom + variables.iconSizeExtraSmall, left: yAxisLabelWidth + GLYPH_PADDING};
235+
const chartPadding = {...CHART_PADDING, bottom: labelSpace + CHART_PADDING.bottom, left: yAxisLabelWidth + GLYPH_PADDING};
257236

258237
if (isLoading || !fontMgr) {
259238
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'LineChartContent', isLoading, isFontLoading: !fontMgr};

0 commit comments

Comments
 (0)