From 620b3277c0c07e98816d9ab26c9775bd627073c8 Mon Sep 17 00:00:00 2001 From: Karl Rezansoff Date: Wed, 1 Dec 2021 22:52:41 -0800 Subject: [PATCH 1/5] moved toast container so toasts show above modal (#1480) --- frontend/src/App/App.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 && } - - Date: Thu, 2 Dec 2021 12:25:40 +0530 Subject: [PATCH 2/5] [Feature] : Gsheet update operation (#1453) * gsheet update op: schema and UI * gsheet update op: server * update operation for google sheet datsource api * remove unsued comments * backward compatiable: removed custom rule for sheet * unsused consolelogs * . * Show gsheets update query body as a single-line codehinter Co-authored-by: Sherfin Shamsudeen --- .../QueryEditors/Googlesheets.schema.json | 71 +++++++++- frontend/src/_components/DynamicForm.jsx | 3 + .../plugins/datasources/googlesheets/index.ts | 13 +- .../datasources/googlesheets/operations.ts | 129 +++++++++++++++++- .../controllers/data_queries.controller.ts | 2 +- 5 files changed, 206 insertions(+), 12 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryEditors/Googlesheets.schema.json b/frontend/src/Editor/QueryManager/QueryEditors/Googlesheets.schema.json index b430247968..cd859f8aea 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/Googlesheets.schema.json +++ b/frontend/src/Editor/QueryManager/QueryEditors/Googlesheets.schema.json @@ -14,10 +14,26 @@ "type": "dropdown-component-flip", "description": "Single select dropdown for operation", "$options": [ - { "value": "read", "name": "Read data from a spreadsheet" }, - { "value": "append", "name": "Append data to a spreadsheet" }, - { "value": "info", "name": "Get spreadsheet info" }, - { "value": "delete_row", "name": "Delete row from a spreadsheet" } + { + "value": "read", + "name": "Read data from a spreadsheet" + }, + { + "value": "append", + "name": "Append data to a spreadsheet" + }, + { + "value": "info", + "name": "Get spreadsheet info" + }, + { + "value": "update", + "name": "Update data to a spreadsheet" + }, + { + "value": "delete_row", + "name": "Delete row from a spreadsheet" + } ] }, "read": { @@ -101,6 +117,51 @@ "lineNumbers": false, "description": "Enter row number" } + }, + "update": { + "spreadsheet_id": { + "$label": "Spreadsheet ID", + "$key": "spreadsheet_id", + "type": "codehinter", + "lineNumbers": false, + "description": "Enter spreadsheet_id" + }, + "where_field": { + "$label": "Where", + "$key": "where_field", + "className": "col-4", + "type": "codehinter", + "lineNumbers": false, + "description": "Enter field" + }, + "where_operation": { + "$label": "Operator", + "$key": "where_operation", + "className": "col-4", + "type": "dropdown", + "description": "Single select dropdown for where operation", + "$options": [ + { + "value": "===", + "name": "===" + } + ] + }, + "where_value": { + "$label": "Value", + "$key": "where_value", + "className": "col-4", + "type": "codehinter", + "lineNumbers": false, + "description": "Enter value" + }, + "body": { + "$label": "Body", + "$key": "body", + "type": "codehinter", + "description": "Enter body", + "lineNumbers": false + } } } -} +} \ No newline at end of file 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() From 372bfe7a184ca2947b29aaf174a5c120676fd88b Mon Sep 17 00:00:00 2001 From: Gandharv Date: Thu, 2 Dec 2021 14:17:48 +0530 Subject: [PATCH 3/5] fix: query manager closing on view updates (#1478) --- .../src/Editor/QueryManager/QueryManager.jsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryManager.jsx b/frontend/src/Editor/QueryManager/QueryManager.jsx index d62dbb10c2..93571d0491 100644 --- a/frontend/src/Editor/QueryManager/QueryManager.jsx +++ b/frontend/src/Editor/QueryManager/QueryManager.jsx @@ -18,7 +18,11 @@ let QueryManager = class QueryManager extends React.Component { constructor(props) { super(props); - this.state = {}; + this.state = { + options: {}, + selectedQuery: null, + selectedDataSource: null, + }; this.previewPanelRef = React.createRef(); } @@ -27,7 +31,7 @@ let QueryManager = class QueryManager extends React.Component { const selectedQuery = props.selectedQuery; const dataSourceId = selectedQuery?.data_source_id; const source = props.dataSources.find((datasource) => 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, + // }); + // } } ); }; From 25a196d354c9d09dc55e0d8d816ea02313d06423 Mon Sep 17 00:00:00 2001 From: Arpit Date: Fri, 3 Dec 2021 13:52:14 +0530 Subject: [PATCH 4/5] fixed: Improper date parsing (#1318) --- .../Editor/Components/Table/Datepicker.jsx | 52 +++++++++++-------- .../src/Editor/Components/Table/Table.jsx | 4 +- .../src/Editor/Inspector/Components/Table.jsx | 15 +++++- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/frontend/src/Editor/Components/Table/Datepicker.jsx b/frontend/src/Editor/Components/Table/Datepicker.jsx index 84c2903bf6..0c6eed954c 100644 --- a/frontend/src/Editor/Components/Table/Datepicker.jsx +++ b/frontend/src/Editor/Components/Table/Datepicker.jsx @@ -5,45 +5,55 @@ import moment from 'moment'; import 'react-datetime/css/react-datetime.css'; import '@/_styles/custom.scss'; -export const Datepicker = function Datepicker({ value, onChange, readOnly, isTimeChecked, dateFormat }) { - const [date, setDate] = React.useState(value); +const getDate = (value, parseDateFormat, displayFormat) => { + 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 76f19473f2..8b43435988 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; @@ -529,10 +530,11 @@ export function Table({ return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} 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' && (
- +
this.onColumnItemChange(index, 'dateFormat', value)} />
+ +
+ { + e.stopPropagation(); + this.onColumnItemChange(index, 'parseDateFormat', e.target.value); + }} + defaultValue={column.parseDateFormat} + placeholder={'DD-MM-YYYY'} + /> +