Skip to content

Commit

Permalink
Merge pull request #47827 from software-mansion-labs/filip-solecki/im…
Browse files Browse the repository at this point in the history
…port-categories-csv

Add CSV import flow to Categories page
  • Loading branch information
mountiny authored Sep 3, 2024
2 parents 094953f + 77bbb95 commit 11952cc
Show file tree
Hide file tree
Showing 48 changed files with 1,602 additions and 45 deletions.
186 changes: 186 additions & 0 deletions assets/images/spreadsheet-computer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/images/table.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@
"react-web-config": "^1.0.0",
"react-webcam": "^7.1.1",
"react-window": "^1.8.9",
"semver": "^7.5.2"
"semver": "^7.5.2",
"xlsx": "file:vendor/xlsx-0.20.3.tgz"
},
"devDependencies": {
"@actions/core": "1.10.0",
Expand Down
30 changes: 30 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ const CONST = {
ALLOWED_RECEIPT_EXTENSIONS: ['jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'],
},

// Allowed extensions for spreadsheets import
ALLOWED_SPREADSHEET_EXTENSIONS: ['xls', 'xlsx', 'csv', 'txt'],

// This is limit set on servers, do not update without wider internal discussion
API_TRANSACTION_CATEGORY_MAX_LENGTH: 255,

Expand Down Expand Up @@ -3916,6 +3919,7 @@ const CONST = {
DROPDOWN_BUTTON_SIZE: {
LARGE: 'large',
MEDIUM: 'medium',
SMALL: 'small',
},

SF_COORDINATES: [-122.4194, 37.7749],
Expand Down Expand Up @@ -5489,6 +5493,32 @@ const CONST = {
REMOVE: 'remove',
},
},

CSV_IMPORT_COLUMNS: {
EMAIL: 'email',
NAME: 'name',
GL_CODE: 'glCode',
SUBMIT_TO: 'submitTo',
APPROVE_TO: 'approveTo',
CUSTOM_FIELD_1: 'customField1',
CUSTOM_FIELD_2: 'customField2',
ROLE: 'role',
REPORT_THRESHHOLD: 'reportThreshold',
APPROVE_TO_ALTERNATE: 'approveToAlternate',
SUBRATE: 'subRate',
AMOUNT: 'amount',
CURRENCY: 'currency',
RATE_ID: 'rateID',
ENABLED: 'enabled',
IGNORE: 'ignore',
},

IMPORT_SPREADSHEET: {
ICON_WIDTH: 180,
ICON_HEIGHT: 160,

CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories',
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ const ONYXKEYS = {
/** Stores the information about currently edited advanced approval workflow */
APPROVAL_WORKFLOW: 'approvalWorkflow',

/** Stores information about recently uploaded spreadsheet file */
IMPORTED_SPREADSHEET: 'importedSpreadsheet',

/** Stores the route to open after changing app permission from settings */
LAST_ROUTE: 'lastRoute',

Expand Down Expand Up @@ -914,6 +917,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip;
[ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
[ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet;
[ONYXKEYS.LAST_ROUTE]: string;
};

Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const,
},
WORKSPACE_CATEGORIES_IMPORT: {
route: 'settings/workspaces/:policyID/categories/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/import` as const,
},
WORKSPACE_CATEGORIES_IMPORTED: {
route: 'settings/workspaces/:policyID/categories/imported',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/imported` as const,
},
WORKSPACE_CATEGORY_CREATE: {
route: 'settings/workspaces/:policyID/categories/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ const SCREENS = {
CATEGORY_GL_CODE: 'Category_GL_Code',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
CATEGORIES_IMPORT: 'Categories_Import',
CATEGORIES_IMPORTED: 'Categories_Imported',
MORE_FEATURES: 'Workspace_More_Features',
MEMBER_DETAILS: 'Workspace_Member_Details',
OWNER_CHANGE_CHECK: 'Workspace_Owner_Change_Check',
Expand Down
5 changes: 3 additions & 2 deletions src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,9 @@ function Button(
<Icon
src={iconRight}
fill={isHovered ? iconHoverFill ?? defaultFill : iconFill ?? defaultFill}
small={medium}
medium={large}
small={small}
medium={medium}
large={large}
/>
) : (
<Icon
Expand Down
23 changes: 15 additions & 8 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ function ButtonWithDropdownMenu<IValueType>({
enterKeyEventListenerPriority = 0,
wrapperStyle,
useKeyboardShortcuts = false,
defaultSelectedIndex = 0,
shouldShowSelectedItemCheck = false,
}: ButtonWithDropdownMenuProps<IValueType>) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedItemIndex, setSelectedItemIndex] = useState(0);
const [selectedItemIndex, setSelectedItemIndex] = useState(defaultSelectedIndex);
const [isMenuVisible, setIsMenuVisible] = useState(false);
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState<AnchorPosition | null>(null);
const {windowWidth, windowHeight} = useWindowDimensions();
Expand Down Expand Up @@ -93,11 +95,12 @@ function ButtonWithDropdownMenu<IValueType>({
isActive: useKeyboardShortcuts,
},
);
const splitButtonWrapperStyle = isSplitButton ? [styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter] : {};

return (
<View style={wrapperStyle}>
{shouldAlwaysShowDropdownMenu || options.length > 1 ? (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, style]}>
<View style={[splitButtonWrapperStyle, style]}>
<Button
success={success}
pressOnEnter={pressOnEnter}
Expand All @@ -108,8 +111,9 @@ function ButtonWithDropdownMenu<IValueType>({
isLoading={isLoading}
shouldRemoveRightBorderRadius
style={[styles.flex1, styles.pr0]}
large={isButtonSizeLarge}
medium={!isButtonSizeLarge}
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
iconRight={Expensicons.DownArrow}
Expand All @@ -125,8 +129,9 @@ function ButtonWithDropdownMenu<IValueType>({
style={[styles.pl0]}
onPress={() => setIsMenuVisible(!isMenuVisible)}
shouldRemoveLeftBorderRadius
large={isButtonSizeLarge}
medium={!isButtonSizeLarge}
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
>
Expand Down Expand Up @@ -154,8 +159,9 @@ function ButtonWithDropdownMenu<IValueType>({
isLoading={isLoading}
text={selectedItem.text}
onPress={(event) => onPress(event, options[0].value)}
large={isButtonSizeLarge}
medium={!isButtonSizeLarge}
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
innerStyles={[innerStyleDropButton]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
/>
Expand All @@ -170,6 +176,7 @@ function ButtonWithDropdownMenu<IValueType>({
onModalShow={onOptionsMenuShow}
onItemSelected={() => setIsMenuVisible(false)}
anchorPosition={popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
anchorRef={nullCheckRef(dropdownAnchor)}
withoutOverlay
anchorAlignment={anchorAlignment}
Expand Down
8 changes: 7 additions & 1 deletion src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type ButtonWithDropdownMenuProps<TValueType> = {
isLoading?: boolean;

/** The size of button size */
buttonSize: ValueOf<typeof CONST.DROPDOWN_BUTTON_SIZE>;
buttonSize?: ValueOf<typeof CONST.DROPDOWN_BUTTON_SIZE>;

/** Should the confirmation button be disabled? */
isDisabled?: boolean;
Expand Down Expand Up @@ -94,6 +94,12 @@ type ButtonWithDropdownMenuProps<TValueType> = {

/** Whether to use keyboard shortcuts for confirmation or not */
useKeyboardShortcuts?: boolean;

/** Decides which index in menuItems should be selected */
defaultSelectedIndex?: number;

/** Whether selected items should be marked as selected */
shouldShowSelectedItemCheck?: boolean;
};

export type {
Expand Down
Loading

0 comments on commit 11952cc

Please sign in to comment.