diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index e8f6a597fe..3c00d663c1 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -66,6 +66,8 @@ class App extends React.Component { return ( <> + + {updateAvailable && ( @@ -96,8 +98,6 @@ class App extends React.Component { {!onboarded && } - - { + const dateString = value; + const momentObj = moment(dateString, parseDateFormat); + const momentString = momentObj.format(displayFormat); + return momentString; +}; - const dateChange = (e) => { - if (isTimeChecked) { - setDate(e.format(`${dateFormat} LT`)); - } else { - setDate(e.format(dateFormat)); - } +export const Datepicker = function Datepicker({ + value, + onChange, + readOnly, + isTimeChecked, + dateDisplayFormat, //?Display date format + parseDateFormat, //?Parse date format +}) { + const [date, setDate] = React.useState(() => getDate(value, parseDateFormat, dateDisplayFormat)); + + const dateChange = (event) => { + const value = event._isAMomentObject ? event.format() : event; + let selectedDateFormat = isTimeChecked ? `${dateDisplayFormat} LT` : dateDisplayFormat; + const dateString = moment(value).format(selectedDateFormat); + setDate(() => dateString); }; React.useEffect(() => { - if (!isTimeChecked) { - setDate(moment(value, 'DD-MM-YYYY').format(dateFormat)); - } - - if (isTimeChecked) { - setDate(moment(value, 'DD-MM-YYYY LT').format(`${dateFormat} LT`)); - } + let selectedDateFormat = isTimeChecked ? `${dateDisplayFormat} LT` : dateDisplayFormat; + const dateString = getDate(value, parseDateFormat, selectedDateFormat); + setDate(() => dateString); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isTimeChecked, readOnly, dateFormat]); - - let inputProps = { - disabled: !readOnly, - }; + }, [isTimeChecked, readOnly, dateDisplayFormat]); const onDatepickerClose = () => { onChange(date); }; + let inputProps = { + disabled: !readOnly, + }; + return ( <> > diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index 9e75de5ad0..b620f2d86c 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -303,6 +303,7 @@ export function Table({ if (columnType === 'datepicker') { column.isTimeChecked = column.isTimeChecked ? column.isTimeChecked : false; column.dateFormat = column.dateFormat ? column.dateFormat : 'DD/MM/YYYY'; + column.parseDateFormat = column.parseDateFormat ?? column.dateFormat; //backwards compatibility } const width = columnSize || defaultColumn.width; @@ -531,10 +532,11 @@ export function Table({ return ( { handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} @@ -665,7 +667,10 @@ export function Table({ ] // Hack: need to fix ); - const data = useMemo(() => tableData, [tableData.length, componentState.changeSet]); + const data = useMemo( + () => tableData, + [tableData.length, componentState.changeSet, component.definition.properties.data.value] + ); const computedStyles = { // width: `${width}px`, diff --git a/frontend/src/Editor/Inspector/Components/Table.jsx b/frontend/src/Editor/Inspector/Components/Table.jsx index bc742d3d85..ac4d835b75 100644 --- a/frontend/src/Editor/Inspector/Components/Table.jsx +++ b/frontend/src/Editor/Inspector/Components/Table.jsx @@ -334,7 +334,7 @@ class Table extends React.Component { {column.columnType === 'datepicker' && ( - Date Format + Date Display Format this.onColumnItemChange(index, 'dateFormat', value)} /> + Date Parse Format + + { + e.stopPropagation(); + this.onColumnItemChange(index, 'parseDateFormat', e.target.value); + }} + defaultValue={column.parseDateFormat} + placeholder={'DD-MM-YYYY'} + /> + datasource.id === dataSourceId); - const paneHeightChanged = this.state.queryPaneHeight !== props.queryPaneHeight; + // const paneHeightChanged = this.state.queryPaneHeight !== props.queryPaneHeight; this.setState( { @@ -56,13 +60,14 @@ let QueryManager = class QueryManager extends React.Component { selectedQuery, queryName: selectedQuery.name, }); - } else { - this.setState({ - options: {}, - selectedQuery: null, - selectedDataSource: paneHeightChanged ? this.state.selectedDataSource : props.selectedDataSource, - }); } + // } else { + // this.setState({ + // options: {}, + // selectedQuery: null, + // selectedDataSource: paneHeightChanged ? this.state.selectedDataSource : props.selectedDataSource, + // }); + // } } ); }; diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index f0c63141e4..620876dcf4 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -30,6 +30,7 @@ const DynamicForm = ({ if (!isEditMode || isEmpty(options)) { optionsChanged(schema?.defaults ?? {}); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const getElement = (type) => { @@ -71,6 +72,7 @@ const DynamicForm = ({ lineNumbers = true, initialValue, height = 'auto', + ignoreBraces = false, }) => { const darkMode = localStorage.getItem('darkMode') === 'true'; switch (type) { @@ -138,6 +140,7 @@ const DynamicForm = ({ theme: darkMode ? 'monokai' : lineNumbers ? 'duotone-light' : 'default', placeholder, height, + ignoreBraces, }; default: return {}; diff --git a/server/plugins/datasources/googlesheets/index.ts b/server/plugins/datasources/googlesheets/index.ts index 5381ec14f3..23db95b12d 100644 --- a/server/plugins/datasources/googlesheets/index.ts +++ b/server/plugins/datasources/googlesheets/index.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { QueryError } from 'src/modules/data_sources/query.error'; import { QueryResult } from 'src/modules/data_sources/query_result.type'; import { QueryService } from 'src/modules/data_sources/query_service.interface'; -import { readData, appendData, deleteData } from './operations'; +import { readData, appendData, deleteData, batchUpdateToSheet } from './operations'; const got = require('got'); @Injectable() @@ -71,6 +71,7 @@ export default class GooglesheetsQueryService implements QueryService { const spreadsheetId = queryOptions['spreadsheet_id']; const spreadsheetRange = queryOptions['spreadsheet_range'] ? queryOptions['spreadsheet_range'] : 'A1:Z500'; const accessToken = sourceOptions['access_token']; + const queryOptionFilter = { key: queryOptions['where_field'], value: queryOptions['where_value'] }; try { switch (operation) { @@ -96,6 +97,16 @@ export default class GooglesheetsQueryService implements QueryService { ); break; + case 'update': + result = await batchUpdateToSheet( + spreadsheetId, + queryOptions['body'], + queryOptionFilter, + queryOptions['where_operation'], + this.authHeader(accessToken) + ); + break; + case 'delete_row': result = await deleteData( spreadsheetId, diff --git a/server/plugins/datasources/googlesheets/operations.ts b/server/plugins/datasources/googlesheets/operations.ts index 9b41753803..a045efc38c 100644 --- a/server/plugins/datasources/googlesheets/operations.ts +++ b/server/plugins/datasources/googlesheets/operations.ts @@ -7,19 +7,85 @@ async function makeRequestToReadValues(spreadSheetId: string, sheet: string, ran } async function makeRequestToAppendValues(spreadSheetId: string, sheet: string, requestBody: any, authHeader: any) { - const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}/values/${ - sheet || '' - }!A:Z:append?valueInputOption=USER_ENTERED`; + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}/values/${sheet || '' + }!A:Z:append?valueInputOption=USER_ENTERED`; return await got.post(url, { headers: authHeader, json: requestBody }).json(); } -async function makeRequestToBatchUpdate(spreadSheetId: string, requestBody: any, authHeader: any) { +async function makeRequestToDeleteRows(spreadSheetId: string, requestBody: any, authHeader: any) { const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}:batchUpdate`; return await got.post(url, { headers: authHeader, json: requestBody }).json(); } +//*BatchUpdate Cell values +async function makeRequestToBatchUpdateValues(spreadSheetId: string, requestBody: any, authHeader: any) { + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}/values:batchUpdate`; + + return await got.post(url, { headers: authHeader, json: requestBody }).json(); +} + +async function makeRequestToLookUpCellValues(spreadSheetId: string, range: string, authHeader: any) { + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}/values/${range}?majorDimension=COLUMNS`; + + return await got.get(url, { headers: authHeader }).json(); +} + +export async function batchUpdateToSheet( + spreadSheetId: string, + requestBody: any, + filterData: any, + filterOperator: string, + authHeader: any +) { + if (!filterOperator) { + return new Error('filterOperator is required'); + } + + const lookUpData = await lookUpSheetData(spreadSheetId, authHeader); + + const updateBody = (requestBody, filterCondition, filterOperator, data) => { + const rowsIndexes = getRowsIndex(filterCondition, filterOperator, data) as any[]; + const colIndexes = getInputKeys(requestBody, data); + + const updateCellIndexes = []; + colIndexes.map((col) => { + rowsIndexes.map((rowIndex) => updateCellIndexes.push({ ...col, cellIndex: `${col.colIndex}${rowIndex}` })); + }); + + const body = []; + Object.entries(requestBody).map((item) => { + updateCellIndexes.map((cell) => { + if (item[0] === cell.col) { + body.push({ cellValue: item[1], cellIndex: cell.cellIndex }); + } + }); + }); + + const _data = body.map((data) => { + return { + majorDimension: 'ROWS', + range: data.cellIndex, + values: [[data.cellValue]], + }; + }); + + return _data; + }; + + const reqBody = { + data: updateBody(requestBody, filterData, filterOperator, lookUpData), + valueInputOption: 'USER_ENTERED', + includeValuesInResponse: true, + }; + + if (!reqBody.data) return new Error('No data to update'); + const response = await makeRequestToBatchUpdateValues(spreadSheetId, reqBody, authHeader); + + return response; +} + export async function readDataFromSheet(spreadSheetId: string, sheet: string, range: string, authHeader: any) { const data = await makeRequestToReadValues(spreadSheetId, sheet, range, authHeader); let headers = []; @@ -83,7 +149,7 @@ async function deleteDataFromSheet(spreadSheetId: string, sheet: string, rowInde ], }; - const response = await makeRequestToBatchUpdate(spreadSheetId, requestBody, authHeader); + const response = await makeRequestToDeleteRows(spreadSheetId, requestBody, authHeader); return response; } @@ -109,3 +175,56 @@ export async function deleteData( ): Promise { return await deleteDataFromSheet(spreadSheetId, sheet, rowIndex, authHeader); } + +async function lookUpSheetData(spreadSheetId: string, authHeader: any) { + const responseLookUpCellValues = await makeRequestToLookUpCellValues(spreadSheetId, 'A1:Z500', authHeader); + const data = await responseLookUpCellValues['values']; + + return data; +} + +//* utils +const getInputKeys = (inputBody, data) => { + const keys = Object.keys(inputBody); + const arr = []; + keys.map((key) => + data.filter((val, index) => { + if (val[0] === key) { + const kIndex = `${String.fromCharCode(65 + index)}`; + arr.push({ col: val[0], colIndex: kIndex }); + } + }) + ); + return arr; +}; + +const getRowsIndex = (inputFilter, filterOperator, response) => { + const filterWithOperator = (type, array, value) => { + switch (type) { + case '===': + return array === value; + default: + return false; + } + }; + const columnValues = response.filter((column) => column[0] === inputFilter.key).flat(); + + if (columnValues.length === 0) { + return -1; + } + + const rowIndex = []; + + columnValues.forEach((col, index) => { + const inputValue = typeof inputFilter.value !== 'string' ? JSON.stringify(inputFilter.value) : inputFilter.value; + const isEqual = filterWithOperator(filterOperator, col, inputValue); + if (isEqual) { + rowIndex.push(index + 1); + } + }); + if (rowIndex.length === 0) { + return -1; + } + + return rowIndex; +}; diff --git a/server/src/controllers/data_queries.controller.ts b/server/src/controllers/data_queries.controller.ts index 9e22fbd746..565350aa9e 100644 --- a/server/src/controllers/data_queries.controller.ts +++ b/server/src/controllers/data_queries.controller.ts @@ -26,7 +26,7 @@ export class DataQueriesController { private dataQueriesService: DataQueriesService, private dataSourcesService: DataSourcesService, private appsAbilityFactory: AppsAbilityFactory - ) {} + ) { } @UseGuards(JwtAuthGuard) @Get()