Skip to content

Commit

Permalink
Merge branch 'Expensify:main' into fix/35391
Browse files Browse the repository at this point in the history
  • Loading branch information
brunovjk authored Apr 22, 2024
2 parents e15e5c0 + 5832dba commit 6120af3
Show file tree
Hide file tree
Showing 206 changed files with 4,898 additions and 2,627 deletions.
8 changes: 1 addition & 7 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,7 @@ module.exports = {
'no-restricted-imports': [
'error',
{
paths: [
...restrictedImportPaths,
{
name: 'underscore',
message: 'Please use the corresponding method from lodash instead',
},
],
paths: restrictedImportPaths,
patterns: restrictedImportPatterns,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const run = () => {

// Initialize string to store Graphite metrics
let graphiteString = '';
let timestamp: number;

// Iterate over each entry
regressionEntries.forEach((entry) => {
Expand All @@ -26,7 +27,9 @@ const run = () => {
const current = JSON.parse(entry);

// Extract timestamp, Graphite accepts timestamp in seconds
const timestamp = current.metadata?.creationDate ? Math.floor(new Date(current.metadata.creationDate).getTime() / 1000) : '';
if (current.metadata?.creationDate) {
timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000);
}

if (current.name && current.meanDuration && current.meanCount && timestamp) {
const formattedName = current.name.split(' ').join('-');
Expand Down
5 changes: 4 additions & 1 deletion .github/actions/javascript/getGraphiteString/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,7 @@ const run = () => {
const regressionEntries = regressionFile.split('\n');
// Initialize string to store Graphite metrics
let graphiteString = '';
let timestamp;
// Iterate over each entry
regressionEntries.forEach((entry) => {
// Skip empty lines
Expand All @@ -2744,7 +2745,9 @@ const run = () => {
try {
const current = JSON.parse(entry);
// Extract timestamp, Graphite accepts timestamp in seconds
const timestamp = current.metadata?.creationDate ? Math.floor(new Date(current.metadata.creationDate).getTime() / 1000) : '';
if (current.metadata?.creationDate) {
timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000);
}
if (current.name && current.meanDuration && current.meanCount && timestamp) {
const formattedName = current.name.split(' ').join('-');
const renderDurationString = `${GRAPHITE_PATH}.${formattedName}.renderDuration ${current.meanDuration} ${timestamp}`;
Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/detectRedirectCycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {parse} from 'csv-parse';
import fs from 'fs';

const parser = parse();

const adjacencyList: Record<string, string[]> = {};
const visited: Map<string, boolean> = new Map<string, boolean>();
const backEdges: Map<string, boolean> = new Map<string, boolean>();

function addEdge(source: string, target: string) {
if (!adjacencyList[source]) {
adjacencyList[source] = [];
}
adjacencyList[source].push(target);
}

function isCyclic(currentNode: string): boolean {
visited.set(currentNode, true);
backEdges.set(currentNode, true);

// Do a depth first search for all the neighbours. If a node is found in backedge, a cycle is detected.
const neighbours = adjacencyList[currentNode];
if (neighbours) {
for (const node of neighbours) {
if (!visited.has(node)) {
if (isCyclic(node)) {
return true;
}
} else if (backEdges.has(node)) {
return true;
}
}
}

backEdges.delete(currentNode);

return false;
}

function detectCycle(): boolean {
for (const [node] of Object.entries(adjacencyList)) {
if (!visited.has(node)) {
if (isCyclic(node)) {
const cycle = Array.from(backEdges.keys());
console.log(`Infinite redirect found in the cycle: ${cycle.join(' -> ')} -> ${node}`);
return true;
}
}
}
return false;
}

fs.createReadStream(`${process.cwd()}/docs/redirects.csv`)
.pipe(parser)
.on('data', (row) => {
// Create a directed graph of sourceURL -> targetURL
addEdge(row[0], row[1]);
})
.on('end', () => {
if (detectCycle()) {
process.exit(1);
}
process.exit(0);
});
19 changes: 15 additions & 4 deletions .github/scripts/verifyRedirect.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@

declare -r REDIRECTS_FILE="docs/redirects.csv"

declare -r RED='\033[0;31m'
declare -r GREEN='\033[0;32m'
declare -r NC='\033[0m'

duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE)
if [[ -n "$duplicates" ]]; then
echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}"
exit 1
fi

if [[ -z "$duplicates" ]]; then
exit 0
npm run detectRedirectCycle
DETECT_CYCLE_EXIT_CODE=$?
if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then
echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}"
exit 1
fi

echo "duplicate redirects are not allowed: $duplicates"
exit 1
echo -e "${GREEN}The redirects.csv is valid!${NC}"
exit 0
12 changes: 8 additions & 4 deletions .github/workflows/deployExpensifyHelp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ concurrency:

jobs:
build:
env:
IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -36,8 +38,8 @@ jobs:

- name: Create docs routes file
run: ./.github/scripts/createDocsRoutes.sh
- name: Check duplicates in redirect.csv

- name: Check for duplicates and cycles in redirects.csv
run: ./.github/scripts/verifyRedirect.sh

- name: Build with Jekyll
Expand All @@ -49,24 +51,26 @@ jobs:
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca
id: deploy
if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork)
if: env.IS_PR_FROM_FORK != 'true'
with:
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: helpdot
directory: ./docs/_site

- name: Setup Cloudflare CLI
if: env.IS_PR_FROM_FORK != 'true'
run: pip3 install cloudflare==2.19.0

- name: Purge Cloudflare cache
if: env.IS_PR_FROM_FORK != 'true'
run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["help.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache
env:
CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }}

- name: Leave a comment on the PR
uses: actions-cool/maintain-one-comment@de04bd2a3750d86b324829a3ff34d47e48e16f4b
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' && env.IS_PR_FROM_FORK != 'true' }}
with:
token: ${{ secrets.OS_BOTIFY_TOKEN }}
body: ${{ format('A preview of your ExpensifyHelp changes have been deployed to {0} ⚡️', steps.deploy.outputs.alias) }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2ePerformanceTests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }}
id: getMergeCommitShaIfUnmergedPR
run: |
git merge --allow-unrelated-histories --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}
git merge --allow-unrelated-histories -X ours --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}
git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}
env:
GITHUB_TOKEN: ${{ github.token }}
Expand Down
8 changes: 8 additions & 0 deletions .well-known/apple-app-site-association
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
"/": "/split/*",
"comment": "Split Expense"
},
{
"/": "/submit/*",
"comment": "Submit Expense"
},
{
"/": "/request/*",
"comment": "Submit Expense"
Expand All @@ -76,6 +80,10 @@
"/": "/search/*",
"comment": "Search"
},
{
"/": "/pay/*",
"comment": "Pay someone"
},
{
"/": "/send/*",
"comment": "Pay someone"
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1001046304
versionName "1.4.63-4"
versionCode 1001046402
versionName "1.4.64-2"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/bank-account"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/iou"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/submit"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/enable-payments"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/statements"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/concierge"/>
Expand All @@ -70,6 +71,7 @@
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/new"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/search"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/send"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/pay"/>
<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/money2020"/>

<!-- Staging URLs -->
Expand All @@ -81,6 +83,7 @@
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/bank-account"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/iou"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/request"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/submit"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/enable-payments"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/statements"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/concierge"/>
Expand All @@ -89,6 +92,7 @@
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/new"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/search"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/send"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/pay"/>
<data android:scheme="https" android:host="staging.new.expensify.com" android:pathPrefix="/money2020"/>
</intent-filter>
</activity>
Expand Down
39 changes: 5 additions & 34 deletions contributingGuides/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ Using arrow functions is the preferred way to write an anonymous function such a

```javascript
// Bad
_.map(someArray, function (item) {...});
someArray.map(function (item) {...});

// Good
_.map(someArray, (item) => {...});
someArray.map((item) => {...});
```

Empty functions (noop) should be declare as arrow functions with no whitespace inside. Avoid _.noop()
Expand Down Expand Up @@ -112,38 +112,9 @@ if (someCondition) {
}
```

## Object / Array Methods

We have standardized on using [underscore.js](https://underscorejs.org/) methods for objects and collections instead of the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods). This is mostly to maintain consistency, but there are some type safety features and conveniences that underscore methods provide us e.g. the ability to iterate over an object and the lack of a `TypeError` thrown if a variable is `undefined`.

```javascript
// Bad
myArray.forEach(item => doSomething(item));
// Good
_.each(myArray, item => doSomething(item));

// Bad
const myArray = Object.keys(someObject).map(key => doSomething(someObject[key]));
// Good
const myArray = _.map(someObject, (value, key) => doSomething(value));

// Bad
myCollection.includes('item');
// Good
_.contains(myCollection, 'item');

// Bad
const modifiedArray = someArray.filter(filterFunc).map(mapFunc);
// Good
const modifiedArray = _.chain(someArray)
.filter(filterFunc)
.map(mapFunc)
.value();
```

## Accessing Object Properties and Default Values

Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this then be explicit about this in your code and check for the type using an underscore method e.g. `_.isBoolean(value)` or `_.isEqual(0)`.
Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this then be explicit about this in your code and check for the type.

```javascript
// Bad
Expand Down Expand Up @@ -448,7 +419,7 @@ const propTypes = {
### Important Note:
In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `<Text>` node and most likely throw an error about trying to render text outside of a `<Text>` component. Use `_.isEmpty()` instead.
In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `<Text>` node and most likely throw an error about trying to render text outside of a `<Text>` component. Use `!!` instead.
```javascript
// Bad! This will cause a breaking an error on native platforms
Expand All @@ -467,7 +438,7 @@ In React Native, one **must not** attempt to falsey-check a string for an inline
{
return (
<View>
{!_.isEmpty(props.title)
{!!props.title
? <View style={styles.title}>{props.title}</View>
: null}
<View style={styles.body}>This is the body</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Assign report approvers to specific employees
description: Create approval hierarchies for reports
---
<div id="expensify-classic" markdown="1">

{% include info.html %}
To assign different approvers for different employees, your workspace must use Advanced Approvals as the report approval workflow.
{% include end-info.html %}

Rather than having one approver for all members of the workspace, you can use the Advanced Approvals workflow to assign different report approvers to specific employees.

To assign a report approver to a specific member of your workspace,
1. Hover over Settings, then click **Workspaces**.
2. Click the desired workspace name.
3. Click the **Members** tab on the left.
4. Click **Settings** next to the desired member.
5. Click the “Approves to” dropdown and select the desired approver for the member’s reports.
6. Click **Save**.

You can also set
- Over-limit approval rules that require a secondary approver when a specific member’s report expenses exceed a set limit.
- Approvers for expenses under a specific tag or category.

</div>
20 changes: 20 additions & 0 deletions docs/articles/expensify-classic/reports/Track-report-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Track report history
description: See the comments and history on a report
---
<div id="expensify-classic" markdown="1">

All changes and comments that have been made on a report are tracked at the bottom of the report.

1. Click the **Reports** tab.
2. Open a report.
3. Scroll to the bottom of the report to review the report’s history and comments.

Additionally, an email notification is sent to the employee when impactful changes are made to the report. For example, if the reimbursable status of an expense is changed, if an expense is approved or denied, or if a comment is added to the report.

</div>





Loading

0 comments on commit 6120af3

Please sign in to comment.