diff --git a/cypress/commons/actions/brewery/BreweryParameters.js b/cypress/commons/actions/brewery/BreweryParameters.js index 38e9c4df6..aadd1f4cc 100644 --- a/cypress/commons/actions/brewery/BreweryParameters.js +++ b/cypress/commons/actions/brewery/BreweryParameters.js @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { BREWERY_SELECTORS } from '../../constants/brewery/IdConstants'; import { GENERIC_SELECTORS } from '../../constants/generic/IdConstants'; +import { apiUtils } from '../../utils'; import { FileParameters, TableParameters, ScenarioParameters } from '../generic'; // Get tabs elements @@ -305,6 +306,15 @@ function clearCustomersTableStringCell(colName, rowIndex, useDelKey = false) { return TableParameters.clearStringCell(getCustomersTable, colName, rowIndex, useDelKey); } +function getCustomersRevertTableButton() { + return TableParameters.getRevertDataButton(getCustomersTable()); +} + +function revertCustomersTable(response = {}) { + apiUtils.interceptPostDatasetTwingraphQuery(response, false); + TableParameters.revertTableData(getCustomersTable()); +} + function getEventsTable() { return cy.get(BREWERY_SELECTORS.scenario.parameters.events.table); } @@ -360,6 +370,10 @@ function clearEventsTableStringCell(colName, rowIndex, useDelKey = false) { return TableParameters.clearStringCell(getEventsTable, colName, rowIndex, useDelKey); } +function getEventsRevertTableButton() { + return TableParameters.getRevertDataButton(getEventsTable()); +} + function getExampleDatasetPart1FileName() { return FileParameters.getFileName(getExampleDatasetPart1()); } @@ -517,6 +531,8 @@ export const BreweryParameters = { deleteRowsCustomersTableData, deleteRowsEventsTableData, editCustomersTableStringCell, + getCustomersRevertTableButton, + revertCustomersTable, getEventsTableLabel, getEventsTableGrid, getEventsImportButton, @@ -531,6 +547,7 @@ export const BreweryParameters = { exportEventsTableDataToCSV, exportEventsTableDataToXLSX, editEventsTableStringCell, + getEventsRevertTableButton, getStock, getRestock, getWaiters, diff --git a/cypress/commons/actions/generic/TableParameters.js b/cypress/commons/actions/generic/TableParameters.js index a0a77478b..fe31cf956 100644 --- a/cypress/commons/actions/generic/TableParameters.js +++ b/cypress/commons/actions/generic/TableParameters.js @@ -99,6 +99,14 @@ function getDeleteRowsDialogConfirmButton() { return cy.get(GENERIC_SELECTORS.genericComponents.table.toolbar.deleteRowsDialogConfirmButton); } +function getRevertDataButton(tableParameterElement) { + return tableParameterElement.find(GENERIC_SELECTORS.genericComponents.table.toolbar.revertButton); +} + +function getRevertDialogConfirmButton() { + return cy.get(GENERIC_SELECTORS.genericComponents.table.toolbar.revertDialogConfirmButton); +} + function getHeader(tableParameterElement) { return getGrid(tableParameterElement).find(GENERIC_SELECTORS.genericComponents.table.header); } @@ -234,6 +242,11 @@ function clearStringCell(getTableElement, colName, rowIndex, useDelKey = false) return getCell(getTableElement(), colName, rowIndex); } +function revertTableData(tableParameterElement) { + getRevertDataButton(tableParameterElement).click(); + getRevertDialogConfirmButton().click(); +} + export const TableParameters = { getFullscreenTable, getLabel, @@ -279,4 +292,6 @@ export const TableParameters = { setFileExportName, editStringCell, clearStringCell, + revertTableData, + getRevertDataButton, }; diff --git a/cypress/commons/constants/generic/IdConstants.js b/cypress/commons/constants/generic/IdConstants.js index 517401dc9..e044b30a0 100644 --- a/cypress/commons/constants/generic/IdConstants.js +++ b/cypress/commons/constants/generic/IdConstants.js @@ -275,6 +275,8 @@ export const GENERIC_SELECTORS = { addRowButton: '[data-cy=add-row-button]', deleteRowsButton: '[data-cy=delete-rows-button]', deleteRowsDialogConfirmButton: '[data-cy=delete-rows-dialog-confirm-button]', + revertButton: '[data-cy=revert-table-button]', + revertDialogConfirmButton: '[data-cy=revert-table-data-dialog-confirm-button]', }, header: '[class=ag-header-container]', placeholder: '[data-cy=empty-table-placeholder]', diff --git a/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js b/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js new file mode 100644 index 000000000..e5e8bb86d --- /dev/null +++ b/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js @@ -0,0 +1,109 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. +import { Login, ScenarioParameters, Scenarios, ScenarioSelector } from '../../commons/actions'; +import { BreweryParameters } from '../../commons/actions/brewery'; +import { stub } from '../../commons/services/stubbing'; +import { apiUtils } from '../../commons/utils'; +import { SOLUTION_WITH_DYNAMIC_TABLE } from '../../fixtures/stubbing/TableParameters-dynamic_table/solution'; +import { DEFAULT_SCENARIOS_LIST } from '../../fixtures/stubbing/default'; + +const EDITED_DATA_CSV = 'customers_from_dataset_edited.csv'; +const twingraphQueryResponse = [ + { + fields: { + name: 'Customer3', + thirsty: false, + satisfaction: 0, + surroundingSatisfaction: 0, + }, + }, + { + fields: { + name: 'Customer1', + thirsty: false, + satisfaction: 0, + surroundingSatisfaction: 0, + }, + }, + { + fields: { + name: 'Customer2', + thirsty: false, + satisfaction: 0, + surroundingSatisfaction: 0, + }, + }, + { + fields: { + name: 'Customer4', + thirsty: false, + satisfaction: 0, + surroundingSatisfaction: 0, + }, + }, +]; + +describe('can use dataset data in editable table', () => { + before(() => { + stub.start(); + stub.setSolutions([SOLUTION_WITH_DYNAMIC_TABLE]); + }); + beforeEach(() => { + Login.login(); + }); + after(() => { + stub.stop(); + }); + it('can display a table filled with data fetched from dataset', () => { + apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false); + Scenarios.getScenarioViewTab(60).should('be.visible'); + ScenarioParameters.expandParametersAccordion(); + BreweryParameters.switchToCustomersTab(); + BreweryParameters.getCustomersTable().should('be.visible'); + BreweryParameters.getCustomersTableLabel().should('be.visible').should('have.text', 'Customers'); + BreweryParameters.getCustomersTableGrid().should('exist'); + BreweryParameters.getCustomersTableCell('name', 0).should('have.text', twingraphQueryResponse[0].fields.name); + }); + it('can export data fetched from dataset and upload a new table', () => { + apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false); + Scenarios.getScenarioViewTab(60).should('be.visible'); + ScenarioParameters.expandParametersAccordion(); + BreweryParameters.switchToCustomersTab(); + BreweryParameters.getCustomersTable().should('be.visible'); + BreweryParameters.getCustomersTableGrid().should('exist'); + BreweryParameters.exportCustomersTableDataToCSV(); + BreweryParameters.importCustomersTableData(EDITED_DATA_CSV); + BreweryParameters.getCustomersTableCell('name', 0).should('have.text', 'Client'); + BreweryParameters.getCustomersTableCell('name', 1).should('have.text', 'Client'); + }); + it('can fetch data from dataset, edit it without saving and revert', () => { + apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false); + Scenarios.getScenarioViewTab(60).should('be.visible'); + ScenarioParameters.expandParametersAccordion(); + BreweryParameters.getEventsRevertTableButton().should('not.exist'); + BreweryParameters.switchToCustomersTab(); + BreweryParameters.getCustomersTableGrid().should('exist'); + BreweryParameters.getCustomersRevertTableButton().should('exist'); + BreweryParameters.editCustomersTableStringCell('name', 0, 'Client').should('have.text', 'Client'); + BreweryParameters.revertCustomersTable(twingraphQueryResponse); + ScenarioParameters.getSaveButton().should('not.exist'); + }); + it('can fetch data from dataset and save table as dataset part, then revert data', () => { + apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false); + Scenarios.getScenarioViewTab(60).should('be.visible'); + ScenarioParameters.expandParametersAccordion(); + BreweryParameters.switchToCustomersTab(); + BreweryParameters.getCustomersTableGrid().should('exist'); + BreweryParameters.editCustomersTableStringCell('name', 0, 'Client').should('have.text', 'Client'); + ScenarioParameters.save({ datasetsEvents: [{ id: 'd-stbddtspr1', securityChanges: { default: 'admin' } }] }); + BreweryParameters.switchToEventsTab(); + ScenarioSelector.selectScenario(DEFAULT_SCENARIOS_LIST[1].name, DEFAULT_SCENARIOS_LIST[1].id); + ScenarioSelector.selectScenario(DEFAULT_SCENARIOS_LIST[0].name, DEFAULT_SCENARIOS_LIST[0].id); + apiUtils.interceptDownloadWorkspaceFile(); + BreweryParameters.switchToCustomersTab(); + BreweryParameters.getCustomersTableGrid().should('exist'); + BreweryParameters.getCustomersTableCell('name', 0).should('have.text', 'Client'); + BreweryParameters.revertCustomersTable(twingraphQueryResponse); + ScenarioParameters.getSaveButton().should('exist'); + }); +}); diff --git a/cypress/fixtures/customers_from_dataset_edited.csv b/cypress/fixtures/customers_from_dataset_edited.csv new file mode 100644 index 000000000..e92e0f2a5 --- /dev/null +++ b/cypress/fixtures/customers_from_dataset_edited.csv @@ -0,0 +1,5 @@ +name,satisfaction,surroundingSatisfaction,thirsty +Client,0,0,true +Client,5,0,false +Customer2,0,0,false +Customer4,8,0,false diff --git a/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js b/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js new file mode 100644 index 000000000..e7735b559 --- /dev/null +++ b/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js @@ -0,0 +1,134 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. +import { DEFAULT_SOLUTION } from '../default'; + +export const SOLUTION_WITH_DYNAMIC_TABLE = { + ...DEFAULT_SOLUTION, + runTemplates: [ + { + id: '3', + name: 'Run template with mock basic types parameters', + description: 'Run template with mock basic types parameters', + csmSimulation: 'BreweryDemoSimulationWithConnector', + tags: ['3', 'Example'], + parameterGroups: ['events', 'customers'], + }, + ], + parameterGroups: [ + { + id: 'events', + labels: { + en: 'Events', + fr: 'Événements', + }, + parameters: ['events'], + }, + { + id: 'customers', + labels: { + en: 'Customers', + fr: 'Clients', + }, + parameters: ['customers'], + }, + ], + parameters: [ + { + id: 'customers', + labels: { + fr: 'Clients', + en: 'Customers', + }, + varType: '%DATASETID%', + defaultValue: null, + minValue: null, + maxValue: null, + regexValidation: null, + options: { + canChangeRowsNumber: true, + connectorId: 'c-d7e5p9o0kjn9', + subType: 'TABLE', + dynamicValues: { + query: + 'MATCH(customer: Customer) WITH {name: customer.id, satisfaction: customer.Satisfaction, ' + + 'surroundingSatisfaction: customer.SurroundingSatisfaction, thirsty: customer.Thirsty} ' + + 'as fields RETURN fields', + resultKey: 'fields', + }, + columns: [ + { + field: 'name', + headerName: 'Name', + type: ['string'], + }, + { + field: 'satisfaction', + headerName: 'Satisfaction', + type: ['int'], + minValue: 0, + maxValue: 10, + acceptsEmptyFields: true, + }, + { + field: 'surroundingSatisfaction', + headerName: 'SurroundingSatisfaction', + type: ['int'], + minValue: 0, + maxValue: 10, + acceptsEmptyFields: true, + }, + { + field: 'thirsty', + headerName: 'Thirsty', + type: ['bool'], + acceptsEmptyFields: true, + }, + ], + }, + }, + { + id: 'events', + labels: { + fr: 'Événements', + en: 'Events', + }, + varType: '%DATASETID%', + options: { + subType: 'TABLE', + columns: [ + { + field: 'theme', + type: ['string'], + }, + { + field: 'date', + type: ['date'], + minValue: '1900-01-01', + maxValue: '2999-12-31', + }, + { + field: 'timeOfDay', + type: ['enum'], + enumValues: ['morning', 'midday', 'afternoon', 'evening'], + }, + { + field: 'eventType', + type: ['string', 'nonResizable', 'nonEditable'], + }, + { + field: 'reservationsNumber', + type: ['int'], + minValue: 0, + maxValue: 300, + acceptsEmptyFields: true, + }, + { + field: 'online', + type: ['bool', 'nonSortable'], + }, + ], + dateFormat: 'dd/MM/yyyy', + }, + }, + ], +}; diff --git a/doc/scenarioParametersConfiguration.md b/doc/scenarioParametersConfiguration.md index dd8011a76..7ea7ebe38 100644 --- a/doc/scenarioParametersConfiguration.md +++ b/doc/scenarioParametersConfiguration.md @@ -267,10 +267,6 @@ mode is enabled by setting `options.subType` to `TABLE`. The `options` dict can - `dateFormat`: a string describing the expected format of dates in the table based on [date-fns format patterns](https://date-fns.org/v2.25.0/docs/parse) (default: `yyyy-MM-dd`) -Also, if you want to have a default table content instead of the default empty table, you can provide the id of an -existing dataset in the `defaultValue` property of the parameter description (this dataset must be a CSV file, with -values separated by commas). - Example: ```yaml @@ -303,6 +299,51 @@ parameters: canChangeRowsNumber: false dateFormat: 'dd/MM/yyyy' ``` +#### Data source +By default, the table component is empty but if you want to display some data, you can define either a default or dynamic +value for the parameter. You can provide the id of an existing dataset in the `defaultValue` property of the parameter +description (this dataset must be a CSV file, with values separated by commas). If you want to fetch data from scenario's +dataset, you can use `dynamicValues` option that uses a cypher query to retrieve data from twingraph dataset. + +_Note: only a **cypher** query from **twingraph** dataset is supported_ + + +`dynamicValues` is an object with the following keys: + +- `query`: the cypher query to run on a twingraph dataset; this query must retrieve a list of property values of the + graph elements, and return them with an alias (example: `MATCH(n:Customer) WITH {name: n.name, age: n.age} as alias +RETURN alias`). Names of the properties must correspond to the `field` key in columns definition. +- `resultKey`: the alias defined in your query; providing this value is required for the webapp to parse the cypher + query results, and retrieve the actual values to display in the table + +Example: +```yaml +parameters: + - id: 'customers' + labels: + fr: 'Clients' + en: 'Customers' + varType: '%DATASETID%' + options: + subType: 'TABLE' + connectorId: 'c-d7e5p9o0kjn9' + description: 'customers data' + dynamicValues: + query: 'MATCH(customer: Customer) WITH {name: customer.id, satisfaction: customer.Satisfaction} as fields RETURN fields' + resultKey: 'fields' + columns: + - field: 'name' + type: + - 'nonEditable' + - field: 'satisfaction' + type: + - 'int' + minValue: 0 + maxValue: 120 + canChangeRowsNumber: false +``` + +_Known issue: Dynamic parameters are not saved as scenario parameters when they are not edited_ #### Columns definition diff --git a/package.json b/package.json index 2663dcf1a..3546abc0b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@cosmotech/api-ts": "^3.1.0-dev", "@cosmotech/azure": "^1.3.4", "@cosmotech/core": "^1.14.0", - "@cosmotech/ui": "^8.1.1", + "@cosmotech/ui": "^9.0.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@microsoft/applicationinsights-web": "^3.0.3", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 437c46ad5..f1e909a79 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -738,7 +738,13 @@ "export": "Export", "addRow": "Add a new row", "deleteRows": "Remove selected rows", - "fullscreen": "Fullscreen" + "fullscreen": "Fullscreen", + "revert": "Revert", + "noDatasetsError": "Impossible to fetch data from dataset because the list of datasets is empty", + "notTwingraphDatasetError": "Only twingraph datasets can be used to fetch table data dynamically", + "noQueryError": "Impossible to fetch data from dataset because there is no twingraph query defined for this parameter. You can load data manually using Import button", + "wrongResultKeyError": "Returned result doesn't have {{resultKey}} property. Probably there is an error in resultKey configuration, please, check your solution", + "wrongQueryError": "Returned result is empty, there is probably an error in your query configuration. Please, check your solution" }, "export": { "labels": { @@ -756,6 +762,13 @@ "cancel": "Cancel", "confirm": "Delete", "checkbox": "Don't show this message again" + }, + "revertDataDialog": { + "title": "Revert table?", + "body": "This will replace your data with initial dataset data.", + "cancel": "Cancel", + "revert": "Revert", + "checkbox": "Don't show this message again" } } }, diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index a299169da..476bff2fb 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -738,7 +738,13 @@ "export": "Exporter", "addRow": "Ajouter une nouvelle ligne", "deleteRows": "Supprimer les lignes sélectionnées", - "fullscreen": "Plein écran" + "fullscreen": "Plein écran", + "revert": "Rétablir", + "noDatasetsError": "Il est impossible de charger les données depuis le dataset parce que la liste des datasets est vide", + "notTwingraphDatasetError": "Le chargement dynamique des données du tableau est disponible uniquement pour les datasets du type twingraph", + "noQueryError": "Il est impossible de charger les données depuis le dataset parce qu'aucune requête du twingraph n'est définie pour ce paramètre. Vous pouvez charger les données manuellement avec le bouton Importer", + "wrongResultKeyError": "Le résultat de la requête ne contient pas de propriété {{resultKey}}. Cela provient probablement d'une erreur dans la configuration de resultKey, veuillez vérifier la solution.", + "wrongQueryError": "Le résultat retourné est vide. Cela provient probablement d'une erreur dans la configuration de la requête. Veuillez vérifier la solution." }, "export": { "labels": { @@ -756,6 +762,13 @@ "cancel": "Annuler", "confirm": "Supprimer", "checkbox": "Ne plus afficher ce message" + }, + "revertDataDialog": { + "title": "Rétablir les données ?", + "body": "Cette action remplacera les données du tableau par les données initiales du dataset.", + "cancel": "Annuler", + "revert": "Rétablir", + "checkbox": "Ne plus afficher ce message" } } }, diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js index 9b78e1b83..cefe6283e 100644 --- a/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js +++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js @@ -8,14 +8,16 @@ import equal from 'fast-deep-equal'; import rfdc from 'rfdc'; import { AgGridUtils, FileBlobUtils } from '@cosmotech/core'; import { Table, TABLE_DATA_STATUS, UPLOAD_FILE_STATUS_KEY } from '@cosmotech/ui'; +import { Api } from '../../../../services/config/Api'; +import { useSetApplicationErrorMessage } from '../../../../state/hooks/ApplicationHooks'; import { useOrganizationId } from '../../../../state/hooks/OrganizationHooks.js'; +import { useCurrentScenarioDatasetList } from '../../../../state/hooks/ScenarioHooks'; import { useWorkspaceId } from '../../../../state/hooks/WorkspaceHooks.js'; import { gridLight, gridDark } from '../../../../theme/'; import { ConfigUtils, TranslationUtils } from '../../../../utils'; import { FileManagementUtils } from '../../../../utils/FileManagementUtils'; import { TableUtils } from '../../../../utils/TableUtils'; -import { TableExportDialog } from './components'; -import { TableDeleteRowsDialog } from './components/TableDeleteRowsDialog'; +import { TableExportDialog, TableRevertDataDialog, TableDeleteRowsDialog } from './components'; const clone = rfdc(); @@ -53,6 +55,8 @@ export const GenericTable = ({ const workspaceId = useWorkspaceId(); const datasets = useSelector((state) => state.dataset?.list?.data); const scenarioId = useSelector((state) => state.scenario?.current?.data?.id); + const currentScenarioDatasetList = useCurrentScenarioDatasetList(); + const setApplicationErrorMessage = useSetApplicationErrorMessage(); const canChangeRowsNumber = ConfigUtils.getParameterAttribute(parameterData, 'canChangeRowsNumber') ?? false; const parameterId = parameterData.id; @@ -86,6 +90,7 @@ export const GenericTable = ({ addRow: t('genericcomponent.table.labels.addRow', 'Add a new row'), deleteRows: t('genericcomponent.table.labels.deleteRows', 'Remove selected rows'), fullscreen: t('genericcomponent.table.labels.fullscreen', 'Fullscreen'), + revert: t('genericcomponent.table.labels.revert', 'Revert'), }; const tableExportDialogLabels = { cancel: t('genericcomponent.table.export.labels.cancel', 'Cancel'), @@ -116,7 +121,7 @@ export const GenericTable = ({ // Store last parameter in a ref // Update a state is async, so, in case of multiple call of updateParameterValue in same function - // parameter state value will be update only in last call. + // parameter state value will be updated only in last call. // We need here to use a ref value for be sure to have the good value. const lastNewParameterValue = useRef(parameter); const updateParameterValue = useCallback( @@ -189,6 +194,97 @@ export const GenericTable = ({ return false; }; + const isDataFetchedFromDataset = !!parameterData?.options?.dynamicValues; + + const _getDataFromTwingraphDataset = async (setClientFileDescriptor) => { + const fileName = `${parameterData.id}.csv`; + setClientFileDescriptor({ + file: null, + content: null, + agGridRows: null, + errors: null, + tableDataStatus: TABLE_DATA_STATUS.DOWNLOADING, + }); + if (_checkForLock()) { + return; + } + GenericTable.downloadLocked[lockId] = true; + try { + if (!currentScenarioDatasetList || currentScenarioDatasetList?.length === 0) + throw new Error( + t( + 'genericcomponent.table.labels.noDatasetsError', + 'Impossible to fetch data from dataset because the list of datasets is empty' + ) + ); + const sourceDatasetId = currentScenarioDatasetList[0]; + const dynamicValuesConfig = ConfigUtils.getParameterAttribute(parameterData, 'dynamicValues'); + const query = dynamicValuesConfig?.query; + if (!query) + throw new Error( + t( + 'genericcomponent.table.labels.noQueryError', + 'Impossible to fetch data from dataset because there is no twingraph query defined for this parameter. ' + + 'You can load data manually using Import button' + ) + ); + const resultKey = dynamicValuesConfig?.resultKey; + const { data } = await Api.Datasets.twingraphQuery(organizationId, sourceDatasetId, { query }); + if (data.length === 0) + throw new Error( + t( + 'genericcomponent.table.labels.wrongQueryError', + 'Returned result is empty, there is probably an error in your query configuration. ' + + 'Please, check your solution' + ) + ); + const rowsDict = data.map((row) => row[resultKey]); + if (rowsDict.includes(undefined)) + throw new Error( + t( + 'genericcomponent.table.labels.wrongResultKeyError', + `Returned result doesn't have ${resultKey} property. + Probably there is an error in resultKey configuration, please, check your solution`, + { resultKey } + ) + ); + const csvRows = AgGridUtils.toCSV(rowsDict, columns); + const agGridData = _generateGridDataFromCSV(csvRows, parameterData); + if (agGridData.error) { + setClientFileDescriptor({ + tableDataStatus: TABLE_DATA_STATUS.ERROR, + errors: agGridData.error, + }); + } else + setClientFileDescriptor({ + name: fileName, + file: null, + agGridRows: agGridData.rows, + errors: null, + status: UPLOAD_FILE_STATUS_KEY.READY_TO_DOWNLOAD, + tableDataStatus: TABLE_DATA_STATUS.READY, + uploadPreprocess: { content: _uploadPreprocess }, + }); + } catch (error) { + const errorComment = error?.response?.status + ? t( + 'genericcomponent.table.labels.notTwingraphDatasetError', + 'Only twingraph datasets can be used to fetch table data dynamically' + ) + : ''; + setApplicationErrorMessage(error, errorComment); + setClientFileDescriptor({ + file: null, + content: null, + agGridRows: null, + errors: null, + tableDataStatus: TABLE_DATA_STATUS.ERROR, + }); + } + + GenericTable.downloadLocked[lockId] = false; + }; + const _downloadDatasetFileContentFromStorage = async ( organizationId, workspaceId, @@ -400,6 +496,8 @@ export const GenericTable = ({ const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const openExportDialog = () => setIsExportDialogOpen(true); const closeExportDialog = () => setIsExportDialogOpen(false); + const [isRevertDialogOpen, setIsRevertDialogOpen] = useState(false); + const closeRevertDialog = () => setIsRevertDialogOpen(false); const exportCSV = useCallback( (fileName) => { const fileContent = AgGridUtils.toCSV(parameter.agGridRows, columns, options); @@ -486,6 +584,13 @@ export const GenericTable = ({ parameter, updateParameterValueWithReset ); + } else if ( + isDataFetchedFromDataset && + !parameter.content && + parameter.status === UPLOAD_FILE_STATUS_KEY.EMPTY && + !alreadyDownloaded + ) { + _getDataFromTwingraphDataset(updateParameterValueWithReset); } }); @@ -567,6 +672,25 @@ export const GenericTable = ({ } else deleteRow(); }, [deleteRow]); + const revertTableWithDatasetData = useCallback( + (isChecked) => { + localStorage.setItem('dontAskAgainToRevertTableData', isChecked); + closeRevertDialog(); + // To trigger isDirty state when an already saved table was reverted and avoid it in other cases, we need to + // updateParameterValue setter and updateOnFirstEdition function that triggers the start of edition; on the other + // hand, updateParameterValueWithReset setter rollbacks modified values without triggering isDirty + _getDataFromTwingraphDataset(parameter.id ? updateParameterValue : updateParameterValueWithReset); + if (parameter.id) updateOnFirstEdition(); + }, + // eslint-disable-next-line + [parameter.id, updateParameterValue, updateParameterValueWithReset, updateOnFirstEdition] + ); + + const onRevertTableData = useCallback(() => { + if (localStorage.getItem('dontAskAgainToRevertTableData') !== 'true') setIsRevertDialogOpen(true); + else revertTableWithDatasetData('true'); + }, [setIsRevertDialogOpen, revertTableWithDatasetData]); + return ( <> + revertTableWithDatasetData(isChecked)} + open={isRevertDialogOpen} + /> ); }; diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js new file mode 100644 index 000000000..247e300ae --- /dev/null +++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js @@ -0,0 +1,34 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { DontAskAgainDialog } from '@cosmotech/ui'; + +export const TableRevertDataDialog = ({ open, onClose, onConfirm }) => { + const { t } = useTranslation(); + const labels = { + title: t('genericcomponent.table.revertDataDialog.title', 'Revert table?'), + body: t('genericcomponent.table.revertDataDialog.body', 'This will replace your data with initial dataset data.'), + cancel: t('genericcomponent.table.revertDataDialog.cancel', 'Cancel'), + confirm: t('genericcomponent.table.revertDataDialog.revert', 'Revert'), + checkbox: t('genericcomponent.table.revertDataDialog.checkbox', "Don't show this message again"), + }; + + return ( + + ); +}; + +TableRevertDataDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, +}; diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js new file mode 100644 index 000000000..f60f46be9 --- /dev/null +++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js @@ -0,0 +1,4 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. + +export { TableRevertDataDialog } from './TableRevertDataDialog'; diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js index eb2dc9de5..f17a4dcec 100644 --- a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js +++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js @@ -2,3 +2,5 @@ // Licensed under the MIT license. export { TableExportDialog } from './TableExportDialog'; +export { TableRevertDataDialog } from './TableRevertDataDialog'; +export { TableDeleteRowsDialog } from './TableDeleteRowsDialog'; diff --git a/src/state/hooks/ScenarioHooks.js b/src/state/hooks/ScenarioHooks.js index 7a078183f..0591f714b 100644 --- a/src/state/hooks/ScenarioHooks.js +++ b/src/state/hooks/ScenarioHooks.js @@ -58,6 +58,10 @@ export const useCurrentScenarioReducerStatus = () => { return useSelector((state) => state.scenario?.current?.status); }; +export const useCurrentScenarioDatasetList = () => { + return useSelector((state) => state.scenario?.current?.data?.datasetList); +}; + export const useResetCurrentScenario = () => { const dispatch = useDispatch(); return useCallback(() => dispatch(dispatchResetCurrentScenario()), [dispatch]); diff --git a/src/utils/ConfigUtils.js b/src/utils/ConfigUtils.js index 93f63d99e..cd62a6ba7 100644 --- a/src/utils/ConfigUtils.js +++ b/src/utils/ConfigUtils.js @@ -73,6 +73,7 @@ const getParameterAttribute = (parameter, attributeName) => { 'canChangeRowsNumber', 'shouldRenameFileOnUpload', 'runTemplateFilter', + 'dynamicValues', ]; if (!knownAttributesNames.includes(attributeName)) { console.warn(`The attribute "${attributeName}" is not a known attribute in the scenario parameters configuration.`); diff --git a/yarn.lock b/yarn.lock index 87f8628cd..e4413efa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1267,10 +1267,10 @@ validator "^13.7.0" xlsx "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz" -"@cosmotech/ui@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@cosmotech/ui/-/ui-8.1.1.tgz#3112e372fe2a2a06f2acaea40084776d232e7188" - integrity sha512-ssyMpJeVAb5nRKIxrxFxTzrDBwhNK5X4uemVPAY/yZZAIsde4585O2A8F2z/xBlVGAkXmN4WwCXLbS9023/ziw== +"@cosmotech/ui@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@cosmotech/ui/-/ui-9.0.0.tgz#59cfebfa02a8964b7e9b8072d9a12a3d1f4b67e8" + integrity sha512-t0kHjyvt8tudjEa8VCMDOjGnJF439irI8yGlM5vTXJQJrXryL2C60Fu6lgBiKfvR6R/FrNictWEmmfbnN5DuzA== dependencies: "@emotion/react" "^11.10.5" "@emotion/styled" "^11.10.5"