Skip to content

Commit

Permalink
Merge branch 'main' into fix/21401-textinput-wont-focus-after-visitin…
Browse files Browse the repository at this point in the history
…g-concierge-from-rhp
  • Loading branch information
bernhardoj committed Apr 24, 2024
2 parents f5fcdd1 + fd8ef99 commit 130d426
Show file tree
Hide file tree
Showing 194 changed files with 3,217 additions and 3,043 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
8 changes: 6 additions & 2 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 @@ -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/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
# - git diff is used to see the files that were added on this branch
# - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main
# - wc counts the words in the result of the intersection
count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'web/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l)
if [ "$count_new_js" -gt "0" ]; then
echo "ERROR: Found new JavaScript files in the project; use TypeScript instead."
exit 1
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ If you're using another operating system, you will need to ensure `mkcert` is in
For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1) for installing cocoapods.

* If you haven't already, install Xcode tools and make sure to install the optional "iOS Platform" package as well. This installation may take awhile.
* After installation, check in System Settings that there's no update for Xcode. Otherwise, you may encounter issues later that don't explain that you solve them by updating Xcode.
* Install project gems, including cocoapods, using bundler to ensure everyone uses the same versions. In the project root, run: `bundle install`
* If you get the error `Could not find 'bundler'`, install the bundler gem first: `gem install bundler` and try again.
* If you are using MacOS and get the error `Gem::FilePermissionError` when trying to install the bundler gem, you're likely using system Ruby, which requires administrator permission to modify. To get around this, install another version of Ruby with a version manager like [rbenv](https://github.com/rbenv/rbenv#installation).
Expand Down
14 changes: 11 additions & 3 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply plugin: "com.google.firebase.firebase-perf"
apply plugin: "fullstory"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"

/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/

/* Fullstory settings */
fullstory {
org 'o-1WN56P-na1'
enabledVariants 'all'
}

react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
Expand Down Expand Up @@ -98,8 +106,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1001046400
versionName "1.4.64-0"
versionCode 1001046500
versionName "1.4.65-0"
// 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 Expand Up @@ -162,7 +170,7 @@ android {
signingConfig null
// buildTypes take precedence over productFlavors when it comes to the signing configuration,
// thus we need to manually set the signing config, so that the e2e uses the debug config again.
// In other words, the signingConfig setting above will be ignored when we build the flavor in release mode.
// In other words, the signingConfig setting above will be ignored when we build the flavor in release mode.
productFlavors.all { flavor ->
// All release builds should be signed with the release config ...
flavor.signingConfig signingConfigs.release
Expand Down
6 changes: 5 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ buildscript {
repositories {
google()
mavenCentral()
maven {url "https://maven.fullstory.com"}
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("com.google.gms:google-services:4.3.4")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
classpath("com.google.firebase:perf-plugin:1.4.1")
// Fullstory integration
classpath ("com.fullstory:gradle-plugin-local:1.45.1")

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
Expand Down Expand Up @@ -70,7 +74,7 @@ allprojects {
// 'mapbox' is the fixed username for Mapbox's Maven repository.
username = 'mapbox'

// The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property.
// The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property.
// Run "npm run setup-mapbox-sdk" to set this property in «USER_HOME»/.gradle/gradle.properties

// Example gradle.properties entry:
Expand Down
17 changes: 17 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ const defaultPlugins = [
// source code transformation as we do not use class property assignment.
'transform-class-properties',

/* Fullstory */
[
'@fullstory/react-native',
{
version: '1.4.0',
org: 'o-1WN56P-na1',
enabledVariants: 'all',
},
],
[
'@fullstory/babel-plugin-annotate-react',
{
native: true,
setFSTagName: true,
},
],

// Keep it last
'react-native-reanimated/plugin',
];
Expand Down
2 changes: 1 addition & 1 deletion config/webpack/webpack.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
{from: 'web/apple-touch-icon.png'},
{from: 'assets/images/expensify-app-icon.svg'},
{from: 'web/manifest.json'},
{from: 'web/gtm.js'},
{from: 'web/thirdPartyScripts.js'},
{from: 'assets/css', to: 'css'},
{from: 'assets/fonts/web', to: 'fonts'},
{from: 'assets/sounds', to: 'sounds'},
Expand Down
48 changes: 41 additions & 7 deletions contributingGuides/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,48 @@ export {
}
```

Using arrow functions is the preferred way to write an anonymous function such as a callback method.
Using named functions is the preferred way to write a callback method.

```javascript
// Bad
_.map(someArray, function (item) {...});
people.map(function (item) {/* Long and complex logic */});
people.map((item) => {/* Long and complex logic with many inner loops*/});
useEffect/useMemo/useCallback(() => {/* Long and complex logic */}, []);

// Good
_.map(someArray, (item) => {...});
function mappingPeople(person) {/* Long and complex logic */};
people.map(mappingPeople);
useEffect/useMemo/useCallback(function handlingConnection() {/* Long and complex logic */}, []);
```

You can still use arrow function for declarations or simple logics to keep them readable.

```javascript
// Bad
randomList.push({
onSelected: Utils.checkIfAllowed(function checkTask() { return Utils.canTeamUp(people); }),
});
routeList.filter(function checkIsActive(route) {
return route.isActive;
});

// Good
randomList.push({
onSelected: Utils.checkIfAllowed(() => Utils.canTeamUp(people)),
});
routeList.filter((route) => route.isActive);
const myFunction = () => {...};
const person = { getName: () => {} };
Utils.connect({
callback: (val) => {},
});
useEffect(() => {
if (isFocused) {
return;
}
setError(null, {});
}, [isFocused]);

```

Empty functions (noop) should be declare as arrow functions with no whitespace inside. Avoid _.noop()
Expand Down Expand Up @@ -123,7 +157,7 @@ myArray.forEach(item => doSomething(item));
_.each(myArray, item => doSomething(item));

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

Expand All @@ -143,7 +177,7 @@ const modifiedArray = _.chain(someArray)

## 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 +482,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 +501,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
2 changes: 1 addition & 1 deletion contributingGuides/TS_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ _.each(arr, () => {});

// GOOD
var arr = [];
arr.forEach(() => {});
arr.forEach(function loopArr() {});

// BAD
lodashGet(object, ['foo'], 'bar');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Assign or remove a Copilot
description: Safely delegate tasks without sharing login information.
---

You can safely delegate tasks to someone else without sharing your login information by assigning them as your Copilot. Your copilot can access your Expensify account through their own account to:
* Prepare expenses on your behalf
* Approve and reimburse expense reports on your behalf (Full Access Copilots only)
* View and make changes to your account, domain, and workspace settings
* View all expenses visible from your account

# Assign a Copilot

1. Hover over Settings and click **Account**.
2. Under Account Details, scroll down to the Copilot: Delegated Access section.
3. Enter the email address or phone number for the person you want to assign as your Copilot.
4. Select whether you want to give your Copilot Full or Submit Only access.
* **Full Access**: Your Copilot will have full access to your account. Nearly every action you can do and everything you can see in your account will also be available to your Copilot. However, Copilots do not have the ability to add or remove other Copilots from your account.
* **Submit Only Access**: Your Copilot will have the same access and limitations as a Full Access Copilot, but they will not be able to approve reports on your behalf—they can only submit them.
5. Click **Invite Copilot**.

If your Copilot already has an Expensify account, they will get an email notifying them that they can now also access your account from within their own. If they do not have an Expensify account, they will get an email with a link to create one. Once created, they will be able to access your account from within their own.

# Remove a Copilot

{% include info.html %}
This action must be completed by the account owner. Copilots cannot remove other Copilots from an account.
{% include end-info.html %}

1. Hover over Settings and click **Account**.
2. Under Account Details, scroll down to the Copilot: Delegated Access section.
3. Click the red X next to the copilot to remove them.

# FAQs

**Can I only have one Copilot?**

You can assign as many Copilots as you like—there is no limit.
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Create a report approval workflow
description: Set up an approval workflow automation for employee reports
---
<div id="expensify-classic" markdown="1">

Expensify allows Workspace Admins to create workflows and automations that determine how expense reports are approved for the workspace. You can choose from three different workflows that either:
- Allow all submitted expenses to be automatically approved (if they don’t have any violations).
- Assign one approver for all reports under the workspace.
- Set up multi-level approvals for more complex workflows.

# Set approval workflow

1. Hover over Settings, then click **Workspaces**.
2. Click the desired workspace name.
3. Click the **Members** tab on the left.
4. Scroll down to the Approval Mode section.
5. Select an approval mode.
- **Submit and Close**: No approval is required. Once a report is submitted, it will be automatically approved and closed. This option may be useful if your expense approvals occur in another system or if the submitter and approver are the same person.
- **Submit and Approve**: All reports go to one person that you assign as the approver. Once a report is submitted, it is sent to the approver. This is the default option.
- **Advanced Approval**: Allows for more complex workflows, like assigning different approvers for different employees or requiring secondary approvals for expenses that exceed a set limit.

To add to your approval workflow, you can also set up approval rules for specific categories and tags.

</div>
Loading

0 comments on commit 130d426

Please sign in to comment.