Merge branch 'develop' of github.com:ToolJet/ToolJet into develop

This commit is contained in:
navaneeth 2021-12-03 16:19:52 +05:30
commit 2a98c6bbae
10 changed files with 273 additions and 46 deletions

View file

@ -66,6 +66,8 @@ class App extends React.Component {
return (
<>
<ToastContainer />
<Router history={history}>
<div className={`main-wrapper ${darkMode ? 'theme-dark' : ''}`}>
{updateAvailable && (
@ -96,8 +98,6 @@ class App extends React.Component {
{!onboarded && <OnboardingModal />}
<ToastContainer />
<PrivateRoute
exact
path="/"

View file

@ -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 (
<>
<Datetime
inputProps={inputProps}
timeFormat={isTimeChecked}
className="cell-type-datepicker"
dateFormat={dateFormat}
dateFormat={dateDisplayFormat}
value={date}
onChange={dateChange}
closeOnSelect={true}
onClose={onDatepickerClose}
/>
</>

View file

@ -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 (
<div>
<Datepicker
dateFormat={column.dateFormat}
dateDisplayFormat={column.dateFormat}
isTimeChecked={column.isTimeChecked}
value={cellValue}
readOnly={column.isEditable}
parseDateFormat={column.parseDateFormat}
onChange={(value) => {
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`,

View file

@ -334,7 +334,7 @@ class Table extends React.Component {
{column.columnType === 'datepicker' && (
<div>
<label className="form-label">Date Format</label>
<label className="form-label">Date Display Format</label>
<div className="field mb-2">
<CodeHinter
currentState={this.props.currentState}
@ -346,6 +346,19 @@ class Table extends React.Component {
onChange={(value) => this.onColumnItemChange(index, 'dateFormat', value)}
/>
</div>
<label className="form-label">Date Parse Format</label>
<div className="field mb-2">
<input
type="text"
className="form-control text-field"
onChange={(e) => {
e.stopPropagation();
this.onColumnItemChange(index, 'parseDateFormat', e.target.value);
}}
defaultValue={column.parseDateFormat}
placeholder={'DD-MM-YYYY'}
/>
</div>
<div className="field mb-2">
<label className="form-check form-switch my-2">
<input

View file

@ -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
}
}
}
}
}

View file

@ -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,
// });
// }
}
);
};

View file

@ -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 {};

View file

@ -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,

View file

@ -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<any> {
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;
};

View file

@ -26,7 +26,7 @@ export class DataQueriesController {
private dataQueriesService: DataQueriesService,
private dataSourcesService: DataSourcesService,
private appsAbilityFactory: AppsAbilityFactory
) {}
) { }
@UseGuards(JwtAuthGuard)
@Get()