Skip to content

Commit

Permalink
Merge pull request #48877 from software-mansion-labs/Guccio/import-ta…
Browse files Browse the repository at this point in the history
…gs-csv

Add import and export CSV flow for Tags
  • Loading branch information
MonilBhavsar committed Sep 17, 2024
2 parents 7893991 + 6674af6 commit 9103d8d
Show file tree
Hide file tree
Showing 20 changed files with 359 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5753,6 +5753,7 @@ const CONST = {
ICON_HEIGHT: 160,

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

Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/gl-code',
getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const,
},
WORKSPACE_TAGS_IMPORT: {
route: 'settings/workspaces/:policyID/tags/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/import` as const,
},
WORKSPACE_TAGS_IMPORTED: {
route: 'settings/workspaces/:policyID/tags/imported',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/imported` as const,
},
WORKSPACE_TAXES: {
route: 'settings/workspaces/:policyID/taxes',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` 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 @@ -411,6 +411,8 @@ const SCREENS = {
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
TAGS_IMPORT: 'Tags_Import',
TAGS_IMPORTED: 'Tags_Imported',
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
REPORT_FIELDS: 'Workspace_ReportFields',
Expand Down
7 changes: 2 additions & 5 deletions src/components/ImportSpreadsheetColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ type ImportSpreadsheetColumnsProps = {
// An optional boolean indicating whether the import button is in a loading state.
isButtonLoading?: boolean;

// A string representing the header text to be rendered.
headerText: string;

// Link to learn more about the file preparation for import.
learnMoreLink?: string;
};

function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, headerText, learnMoreLink}: ImportSpreadsheetColumnsProps) {
function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
Expand All @@ -55,7 +52,7 @@ function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles
<ScrollView>
<View style={styles.mh5}>
<Text>
{headerText}
{translate('spreadsheet.importDescription')}
<TextLink href={learnMoreLink ?? ''}>{` ${translate('common.learnMore')}`}</TextLink>
</Text>

Expand Down
6 changes: 5 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,10 @@ export default {
singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
importSuccessfullTitle: 'Import successful',
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'),
importFailedTitle: 'Import failed',
importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.',
importDescription: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.',
sizeNotMet: 'File size must be greater than 0 bytes',
invalidFileMessage:
'The file you uploaded is either empty or contains invalid data. Please ensure that the file is correctly formatted and contains the necessary information before uploading it again.',
Expand Down Expand Up @@ -2968,7 +2970,6 @@ export default {
glCode: 'GL code',
updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.',
importCategories: 'Import categories',
importedCategoriesMessage: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.',
},
moreFeatures: {
subtitle: 'Use the toggles below to enable more features as you grow. Each feature will appear in the navigation menu for further customization.',
Expand Down Expand Up @@ -3181,6 +3182,9 @@ export default {
importedFromAccountingSoftware: 'The tags below are imported from your',
glCode: 'GL code',
updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.',
importTags: 'Import tags',
importedTagsMessage: (columnCounts: number) =>
`We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`,
},
taxes: {
subtitle: 'Add tax names, rates, and set defaults.',
Expand Down
6 changes: 5 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,9 @@ export default {
importFailedTitle: 'Fallo en la importación',
importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.',
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'),
importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'),
importSuccessfullTitle: 'Importar categorías',
importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.',
sizeNotMet: 'El archivo adjunto debe ser más grande que 0 bytes.',
invalidFileMessage:
'El archivo que subiste está vacío o contiene datos no válidos. Asegúrate de que el archivo esté correctamente formateado y contenga la información necesaria antes de volver a subirlo.',
Expand Down Expand Up @@ -3016,7 +3018,6 @@ export default {
glCode: 'Código de Libro Mayor',
updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Inténtelo nuevamente.',
importCategories: 'Importar categorías',
importedCategoriesMessage: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.',
},
moreFeatures: {
subtitle: 'Utiliza los botones de abajo para activar más funciones a medida que creces. Cada función aparecerá en el menú de navegación para una mayor personalización.',
Expand Down Expand Up @@ -3230,6 +3231,9 @@ export default {
importedFromAccountingSoftware: 'Etiquetas importadas desde',
glCode: 'Código de Libro Mayor',
updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.',
importTags: 'Importar categorías',
importedTagsMessage: (columnCounts: number) =>
`Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`,
},
taxes: {
subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/ExportTagsSpreadsheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type ExportTagsSpreadsheetParams = {
/** ID of the policy */
policyID: string;
};

export default ExportTagsSpreadsheetParams;
12 changes: 12 additions & 0 deletions src/libs/API/parameters/ImportTagsSpreadsheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type ImportTagsSpreadsheetParams = {
/** ID of the policy to which the tags will be imported */
policyID: string;

/**
* Stringified JSON object with type of following structure:
* Array<{name: string, enabled: boolean, 'GL Code': string}>
*/
tags: string;
};

export default ImportTagsSpreadsheetParams;
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,9 @@ export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPoli
export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams';
export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './UpdateQuickbooksOnlineAutoCreateVendorParams';
export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet';
export type {default as ImportTagsSpreadsheetParams} from './ImportTagsSpreadsheet';
export type {default as ExportCategoriesSpreadsheetParams} from './ExportCategoriesSpreadsheet';
export type {default as ExportTagsSpreadsheetParams} from './ExportTagsSpreadsheet';
export type {default as UpdateXeroGenericTypeParams} from './UpdateXeroGenericTypeParams';
export type {default as UpdateCardSettlementFrequencyParams} from './UpdateCardSettlementFrequencyParams';
export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSettlementAccountParams';
Expand Down
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ const WRITE_COMMANDS = {
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
IMPORT_TAGS_SREADSHEET: 'ImportTagsSpreadsheet',
IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet',
EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV',
EXPORT_TAGS_CSV: 'ExportTagsCSV',
RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory',
CREATE_POLICY_TAG: 'CreatePolicyTag',
RENAME_POLICY_TAG: 'RenamePolicyTag',
Expand Down Expand Up @@ -524,8 +526,10 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
[WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET]: Parameters.ImportTagsSpreadsheetParams;
[WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams;
[WRITE_COMMANDS.EXPORT_CATEGORIES_CSV]: Parameters.ExportCategoriesSpreadsheetParams;
[WRITE_COMMANDS.EXPORT_TAGS_CSV]: Parameters.ExportTagsSpreadsheetParams;
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT]: () =>
require<ReactComponentModule>('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT]: () => require<ReactComponentModule>('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage').default,
[SCREENS.WORKSPACE.TAGS_IMPORT]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/ImportTagsPage').default,
[SCREENS.WORKSPACE.TAGS_IMPORTED]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/ImportedTagsPage').default,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default,
[SCREENS.WORKSPACE.TAG_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/TagSettingsPage').default,
[SCREENS.WORKSPACE.TAG_LIST_VIEW]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/WorkspaceViewTagsPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.TAG_EDIT,
SCREENS.WORKSPACE.TAG_LIST_VIEW,
SCREENS.WORKSPACE.TAG_GL_CODE,
SCREENS.WORKSPACE.TAGS_IMPORT,
SCREENS.WORKSPACE.TAGS_IMPORTED,
],
[SCREENS.WORKSPACE.CATEGORIES]: [
SCREENS.WORKSPACE.CATEGORY_CREATE,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,12 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
orderWeight: Number,
},
},
[SCREENS.WORKSPACE.TAGS_IMPORT]: {
path: ROUTES.WORKSPACE_TAGS_IMPORT.route,
},
[SCREENS.WORKSPACE.TAGS_IMPORTED]: {
path: ROUTES.WORKSPACE_TAGS_IMPORTED.route,
},
[SCREENS.WORKSPACE.TAG_CREATE]: {
path: ROUTES.WORKSPACE_TAG_CREATE.route,
},
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_IMPORT]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAGS_IMPORTED]: {
policyID: string;
};
[SCREENS.WORKSPACE.TAG_SETTINGS]: {
policyID: string;
orderWeight: number;
Expand Down
4 changes: 3 additions & 1 deletion src/libs/actions/Policy/Category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,14 @@ function downloadCategoriesCSV(policyID: string) {
policyID,
});

const fileName = 'Categories.csv';

const formData = new FormData();
Object.entries(finalParameters).forEach(([key, value]) => {
formData.append(key, String(value));
});

fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), 'Categories.csv', '', false, formData, CONST.NETWORK.METHOD.POST);
fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST);
}

function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) {
Expand Down
60 changes: 60 additions & 0 deletions src/libs/actions/Policy/Tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import type {
UpdatePolicyTagGLCodeParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import {translateLocal} from '@libs/Localize';
import Log from '@libs/Log';
import enhanceParameters from '@libs/Network/enhanceParameters';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {navigateWhenEnableFeature} from '@libs/PolicyUtils';
Expand Down Expand Up @@ -122,6 +126,34 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTag
return newOptimisticPolicyRecentlyUsedTags;
}

function updateImportSpreadsheetData(tagsLength: number): OnyxData {
const onyxData: OnyxData = {
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.IMPORTED_SPREADSHEET,
value: {
shouldFinalModalBeOpened: true,
importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', tagsLength)},
},
},
],

failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.IMPORTED_SPREADSHEET,
value: {
shouldFinalModalBeOpened: true,
importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')},
},
},
],
};

return onyxData;
}

function createPolicyTag(policyID: string, tagName: string) {
const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[0] ?? {};
const newTagName = PolicyUtils.escapeTagName(tagName);
Expand Down Expand Up @@ -187,6 +219,18 @@ function createPolicyTag(policyID: string, tagName: string) {
API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData);
}

function importPolicyTags(policyID: string, tags: PolicyTag[]) {
const onyxData = updateImportSpreadsheetData(tags.length);

const parameters = {
policyID,
// eslint-disable-next-line @typescript-eslint/naming-convention
tags: JSON.stringify(tags.map((tag) => ({name: tag.name, enabled: tag.enabled, 'GL Code': tag['GL Code']}))),
};

API.write(WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET, parameters, onyxData);
}

function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record<string, {name: string; enabled: boolean}>, tagListIndex: number) {
const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {};

Expand Down Expand Up @@ -846,6 +890,20 @@ function setPolicyTagGLCode(policyID: string, tagName: string, tagListIndex: num
API.write(WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE, parameters, onyxData);
}

function downloadTagsCSV(policyID: string) {
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_TAGS_CSV, {
policyID,
});
const fileName = 'Tags.csv';

const formData = new FormData();
Object.entries(finalParameters).forEach(([key, value]) => {
formData.append(key, String(value));
});

fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_TAGS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST);
}

export {
buildOptimisticPolicyRecentlyUsedTags,
setPolicyRequiresTag,
Expand All @@ -861,6 +919,8 @@ export {
renamePolicyTaglist,
setWorkspaceTagEnabled,
setPolicyTagGLCode,
importPolicyTags,
downloadTagsCSV,
};

export type {NewCustomUnit};
5 changes: 1 addition & 4 deletions src/pages/workspace/categories/ImportedCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
const policy = usePolicy(policyID);
const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0);

const isControl = isControlPolicy(policy);

const getColumnRoles = (): ColumnRole[] => {
const roles = [];
roles.push(
Expand All @@ -42,7 +40,7 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
{text: translate('common.enabled'), value: CONST.CSV_IMPORT_COLUMNS.ENABLED, isRequired: true},
);

if (isControl) {
if (isControlPolicy(policy)) {
roles.push({text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE});
}

Expand Down Expand Up @@ -134,7 +132,6 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) {
errors={isValidationEnabled ? validate() : undefined}
columnRoles={columnRoles}
isButtonLoading={isImportingCategories}
headerText={translate('workspace.categories.importedCategoriesMessage')}
learnMoreLink={CONST.IMPORT_SPREADSHEET.CATEGORIES_ARTICLE_LINK}
/>

Expand Down
21 changes: 21 additions & 0 deletions src/pages/workspace/tags/ImportTagsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import ImportSpreedsheet from '@components/ImportSpreadsheet';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';

type ImportTagsPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.TAGS_IMPORT>;

function ImportTagsPage({route}: ImportTagsPageProps) {
const policyID = route.params.policyID;

return (
<ImportSpreedsheet
backTo={ROUTES.WORKSPACE_TAGS.getRoute(policyID)}
goTo={ROUTES.WORKSPACE_TAGS_IMPORTED.getRoute(policyID)}
/>
);
}

export default ImportTagsPage;
Loading

0 comments on commit 9103d8d

Please sign in to comment.