mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-21 07:59:01 +00:00
Merge branch 'develop' of github.com:ToolJet/ToolJet into develop
This commit is contained in:
commit
2a98c6bbae
10 changed files with 273 additions and 46 deletions
|
|
@ -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="/"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class DataQueriesController {
|
|||
private dataQueriesService: DataQueriesService,
|
||||
private dataSourcesService: DataSourcesService,
|
||||
private appsAbilityFactory: AppsAbilityFactory
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
|
|
|
|||
Loading…
Reference in a new issue