From ad9e5256b444c91ef6b2288bee0fdd440442c266 Mon Sep 17 00:00:00 2001 From: Kartik Gupta Date: Tue, 29 Oct 2024 15:02:09 +0530 Subject: [PATCH 001/104] fix editing version opening when app is launched --- frontend/src/AppBuilder/_hooks/useAppData.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 26d847ae14..0a01262e09 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -138,7 +138,8 @@ const useAppData = (appId, moduleId, mode = 'edit', { environmentId, versionId } return; } const queryParams = getPreviewQueryParams(); - const isPublicAccess = currentSession?.load_app && currentSession?.authentication_failed; + const isPublicAccess = + (currentSession?.load_app && currentSession?.authentication_failed) || (!queryParams.version && mode !== 'edit'); const isPreviewForVersion = (mode !== 'edit' && queryParams.version) || isPublicAccess; let appDataPromise; if (isPublicAccess) { From 8d492d93945ee336dfc46f6fc12a92feed4b7afd Mon Sep 17 00:00:00 2001 From: Muhsin Shah C P Date: Wed, 6 Nov 2024 15:02:41 +0530 Subject: [PATCH 002/104] Added region (#11223) --- frontend/src/modules/common/helpers/index.js | 3 +- .../src/modules/common/helpers/timeUtils.js | 29 +++++++++++++++++++ .../services/onboarding.service.ce.js | 2 ++ server/ce/onboarding/service.ts | 4 +-- server/src/dto/user.dto.ts | 5 ++++ server/src/services/metadata.service.ts | 8 +++-- 6 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 frontend/src/modules/common/helpers/timeUtils.js diff --git a/frontend/src/modules/common/helpers/index.js b/frontend/src/modules/common/helpers/index.js index 3af21c238a..8d88f2c938 100644 --- a/frontend/src/modules/common/helpers/index.js +++ b/frontend/src/modules/common/helpers/index.js @@ -1,3 +1,4 @@ import * as utils from './utils'; +import * as timeUtils from './timeUtils'; -export { utils }; +export { utils, timeUtils }; diff --git a/frontend/src/modules/common/helpers/timeUtils.js b/frontend/src/modules/common/helpers/timeUtils.js new file mode 100644 index 0000000000..fbab12b2cb --- /dev/null +++ b/frontend/src/modules/common/helpers/timeUtils.js @@ -0,0 +1,29 @@ +const moment = require('moment-timezone'); +/** + * Get the user's country based on their time zone. + * @param {string} userTimeZone - The user's time zone. + * @returns {string} The user's country or the original time zone if not found. + */ +export function getCountryByTimeZone() { + let userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + /* Some times the browser gives Asia/Calcutta */ + if (userTimeZone === 'Asia/Calcutta') { + userTimeZone = 'Asia/Kolkata'; + } + // Get a list of countries from moment-timezone + const countries = moment.tz.countries(); + + // Iterate through the countries and check if the time zone is associated with any country + for (const country of countries) { + const timeZones = moment.tz.zonesForCountry(country); + + if (timeZones.includes(userTimeZone)) { + // Use Intl.DisplayNames to get the full country name + const countryName = new Intl.DisplayNames(['en'], { type: 'region' }).of(country); + return countryName; + } + } + + // Return the original time zone if no matching country is found + return userTimeZone; +} diff --git a/frontend/src/modules/onboarding/services/onboarding.service.ce.js b/frontend/src/modules/onboarding/services/onboarding.service.ce.js index 0f06e9b2f4..3cdf7f52b4 100644 --- a/frontend/src/modules/onboarding/services/onboarding.service.ce.js +++ b/frontend/src/modules/onboarding/services/onboarding.service.ce.js @@ -2,6 +2,7 @@ import config from 'config'; import { handleResponse } from '@/_helpers'; import { updateCurrentSession } from '@/_helpers/authorizeWorkspace'; import queryString from 'query-string'; +import { getCountryByTimeZone } from '@/modules/common/helpers/timeUtils'; function setupFirstUser({ companyName, buildPurpose, name, workspaceName, password, email }) { const requestOptions = { @@ -15,6 +16,7 @@ function setupFirstUser({ companyName, buildPurpose, name, workspaceName, passwo workspaceName, email, password, + region: getCountryByTimeZone(), }), }; return fetch(`${config.apiUrl}/onboarding/setup-first-user`, requestOptions) diff --git a/server/ce/onboarding/service.ts b/server/ce/onboarding/service.ts index c93c5b4a29..676505b9ef 100644 --- a/server/ce/onboarding/service.ts +++ b/server/ce/onboarding/service.ts @@ -22,7 +22,7 @@ export class OnboardingService { ) {} async setupFirstUser(response: Response, userCreateDto: CreateAdminDto): Promise { - const { name, workspaceName, password, email } = userCreateDto; + const { name, workspaceName, password, email, region } = userCreateDto; const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace'); const result = await dbTransactionWrap(async (manager) => { @@ -52,7 +52,7 @@ export class OnboardingService { return this.authService.generateLoginResultPayload(response, user, organization, false, true, null, manager); }); - await this.metadataService.finishOnboardingCE(name, email, workspaceName); + await this.metadataService.finishOnboardingCE(name, email, workspaceName, region); return result; } } diff --git a/server/src/dto/user.dto.ts b/server/src/dto/user.dto.ts index ad9daf44d7..b710e8e7b3 100644 --- a/server/src/dto/user.dto.ts +++ b/server/src/dto/user.dto.ts @@ -100,6 +100,11 @@ export class CreateAdminDto { @IsNotEmpty() @Transform(({ value }) => sanitizeInput(value)) workspaceName: string; + + @IsString() + @IsOptional() + @Transform(({ value }) => sanitizeInput(value)) + region?: string; } export class OnboardUserDto extends CreateUserDto { @IsString() diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index bf3a6f46ba..4d37f321e6 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -50,7 +50,7 @@ export class MetadataService { async finishOnboarding(name, email, companyName, companySize, role) { if (process.env.NODE_ENV == 'production') { const metadata = await this.getMetaData(); - void this.finishInstallation(name, email, companyName, companySize, role, metadata); + void this.finishInstallation(name, email, companyName, null, companySize, role, metadata); await this.updateMetaData({ onboarded: true, @@ -58,10 +58,10 @@ export class MetadataService { } } - async finishOnboardingCE(name: string, email: string, companyName: string) { + async finishOnboardingCE(name: string, email: string, companyName: string, region: string) { if (process.env.NODE_ENV == 'production') { const metadata = await this.getMetaData(); - void this.finishInstallation(name, email, companyName, null, null, metadata); + void this.finishInstallation(name, email, companyName, region, null, null, metadata); await this.updateMetaData({ onboarded: true, @@ -73,6 +73,7 @@ export class MetadataService { name: string, email: string, org: string, + region: string, companySize?: string, role?: string, metadata?: Metadata @@ -88,6 +89,7 @@ export class MetadataService { org, companySize, role, + region, }, }); } catch (error) { From 7791e850b04c786ee892959d3695abcc97dc05a1 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 00:40:05 +0530 Subject: [PATCH 003/104] Fixed issue causing dropdown & multiselect to not work properly --- .../AppBuilder/Widgets/Table/Components/TableRow.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx b/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx index 8db79b1b5d..221e6b4594 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Components/TableRow.jsx @@ -85,16 +85,20 @@ export const TableRow = React.memo( : '' }`} {...rowProps} - onClickCapture={async () => { + onClickCapture={() => { // toggleRowSelected will triggered useRededcuer function in useTable and in result will get the selectedFlatRows consisting row which are selected + const selectedRow = row.original; + const selectedRowId = row.id; + setExposedVariables({ selectedRow, selectedRowId }); + fireEvent('onRowClicked'); + }} + onClick={async () => { if (allowSelection) { await toggleRowSelected(row.id); } const selectedRow = row.original; const selectedRowId = row.id; - setExposedVariables({ selectedRow, selectedRowId }); mergeToTableDetails({ selectedRow, selectedRowId }); - fireEvent('onRowClicked'); }} onMouseOver={() => { if (hoverAdded) { From 365fa7090c4a8051d05a016b16e10a5221266f6f Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Mon, 28 Oct 2024 03:08:09 +0530 Subject: [PATCH 004/104] Event missing updated exposedVariables fix --- frontend/src/AppBuilder/Widgets/Modal.jsx | 31 +++++------ frontend/src/Editor/Components/Checkbox.jsx | 43 ++++++--------- frontend/src/Editor/Components/Datepicker.jsx | 41 +++++--------- frontend/src/Editor/Components/DropDown.jsx | 39 +++++--------- .../Components/DropdownV2/DropdownV2.jsx | 45 +++++++--------- .../MultiselectV2/MultiselectV2.jsx | 49 +++++++++-------- .../src/Editor/Components/NumberInput.jsx | 53 +++++++------------ .../src/Editor/Components/PasswordInput.jsx | 33 ++++++------ frontend/src/Editor/Components/TextInput.jsx | 35 ++++++------ frontend/src/Editor/Components/ToggleV2.jsx | 27 +++++----- 10 files changed, 167 insertions(+), 229 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx index 7632770d91..a88aff630f 100644 --- a/frontend/src/AppBuilder/Widgets/Modal.jsx +++ b/frontend/src/AppBuilder/Widgets/Modal.jsx @@ -72,34 +72,22 @@ export const Modal = function Modal({ setShowModal(true); }, close: async function () { - setShowModal(false); setExposedVariable('show', false); + setShowModal(false); }, }; setExposedVariables(exposedVariables); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - if (isInitialRender.current) { - isInitialRender.current = false; - return; - } - - fireEvent(!showModal ? 'onClose' : 'onOpen'); - const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0]; - inputRef?.blur(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showModal]); - function hideModal() { - setShowModal(false); setExposedVariable('show', false); + setShowModal(false); } function openModal() { - setShowModal(true); setExposedVariable('show', true); + setShowModal(true); } useEffect(() => { @@ -149,6 +137,19 @@ export const Modal = function Modal({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [showModal, modalHeight]); + + useEffect(() => { + if (isInitialRender.current) { + isInitialRender.current = false; + return; + } + + fireEvent(!showModal ? 'onClose' : 'onOpen'); + const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0]; + inputRef?.blur(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showModal]); + const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false; const customStyles = { diff --git a/frontend/src/Editor/Components/Checkbox.jsx b/frontend/src/Editor/Components/Checkbox.jsx index 5b0021114c..3a9a8c1ebb 100644 --- a/frontend/src/Editor/Components/Checkbox.jsx +++ b/frontend/src/Editor/Components/Checkbox.jsx @@ -20,7 +20,6 @@ export const Checkbox = ({ const isMandatory = validation?.mandatory ?? false; const [defaultValue, setDefaultValue] = useState(defaultValueFromProperties); const [checked, setChecked] = useState(defaultValueFromProperties); - const [value, setValue] = React.useState(defaultValueFromProperties); const [userInteracted, setUserInteracted] = useState(false); const { label } = properties; @@ -33,14 +32,12 @@ export const Checkbox = ({ const [loading, setLoading] = useState(properties?.loadingState); const [disable, setDisable] = useState(disabledState || loadingState); const [visibility, setVisibility] = useState(properties.visibility); - const { isValid, validationError } = validate(checked); + const [validationStatus, setValidationStatus] = useState(validate(checked)); + const { isValid, validationError } = validationStatus; const toggleValue = (e) => { const isChecked = e.target.checked; - setChecked(isChecked); - setValue(isChecked); - - setExposedVariable('value', isChecked); + setInputValue(isChecked); if (isChecked) { fireEvent('onCheck'); } else { @@ -52,9 +49,8 @@ export const Checkbox = ({ useEffect(() => { if (isInitialRender.current) return; setDefaultValue(defaultValueFromProperties); - setChecked(defaultValueFromProperties); - setValue(defaultValueFromProperties); - setExposedVariable('value', defaultValueFromProperties); + setInputValue(defaultValueFromProperties); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValueFromProperties]); @@ -102,22 +98,14 @@ export const Checkbox = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - useEffect(() => { const setCheckedAndNotify = async (status) => { - await setExposedVariable('value', status); + setInputValue(status); if (status) { fireEvent('onCheck'); } else { fireEvent('onUnCheck'); } - setChecked(status); - setValue(status); }; const exposedVariables = { @@ -138,10 +126,8 @@ export const Checkbox = ({ }, toggle: () => { setExposedVariable('toggle', async function () { - setExposedVariable('value', !checked); + setInputValue(!checked); fireEvent('onChange'); - setChecked(!checked); - setValue(!checked); setUserInteracted(true); }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -154,10 +140,6 @@ export const Checkbox = ({ isValid: isValid, }; - setDefaultValue(defaultValueFromProperties); - setChecked(defaultValueFromProperties); - setValue(defaultValueFromProperties); - setExposedVariables(exposedVariables); isInitialRender.current = false; @@ -167,9 +149,7 @@ export const Checkbox = ({ const handleToggleChange = () => { const newCheckedState = !checked; - setChecked(newCheckedState); - setValue(newCheckedState); - setExposedVariable('value', newCheckedState); + setInputValue(newCheckedState); fireEvent('onChange'); if (newCheckedState) { fireEvent('onCheck'); @@ -179,6 +159,13 @@ export const Checkbox = ({ setUserInteracted(true); }; + const setInputValue = (value) => { + setChecked(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ value, isValid: validationStatus?.isValid }); + }; + const renderCheckBox = () => ( <>
{ setShowValidationError(true); - setDate(date); - const dateString = computeDateString(date); - setExposedVariable('value', dateString); + setInputValue(date); fireEvent('onSelect'); }; @@ -51,11 +52,9 @@ export const Datepicker = function Datepicker({ if (isInitialRender.current) return; const dateMomentInstance = defaultValue && moment(defaultValue, selectedDateFormat); if (dateMomentInstance && dateMomentInstance.isValid()) { - setDate(dateMomentInstance.toDate()); - setExposedVariable('value', defaultValue); + setInputValue(dateMomentInstance.toDate()); } else { - setDate(null); - setExposedVariable('value', undefined); + setInputValue(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]); @@ -73,32 +72,20 @@ export const Datepicker = function Datepicker({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disabledDates, format]); - const validationData = validate(computeDateString(date)); - const { isValid, validationError } = validationData; - useEffect(() => { - isInitialRender.current = false; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - - useEffect(() => { - const exposedVariables = { - isValid, - }; const dateMomentInstance = defaultValue && moment(defaultValue, selectedDateFormat); - if (dateMomentInstance && dateMomentInstance.isValid()) { - setDate(dateMomentInstance.toDate()); - exposedVariables.value = defaultValue; - } else { - setDate(null); - exposedVariables.value = undefined; - } - setExposedVariables(exposedVariables); + setInputValue(dateMomentInstance && dateMomentInstance.isValid() ? dateMomentInstance.toDate() : null); isInitialRender.current = false; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const setInputValue = (value) => { + setDate(value); + const validationStatus = validate(computeDateString(value)); + setValidationStatus(validationStatus); + setExposedVariables({ value: value ? computeDateString(value) : undefined, isValid: validationStatus?.isValid }); + }; + return (
(advanced ? findDefaultItem(schema) : value)); const [showValidationError, setShowValidationError] = useState(false); - const validationData = validate(value); - const { isValid, validationError } = validationData; + const [validationStatus, setValidationStatus] = useState(validate(value)); + const { isValid, validationError } = validationStatus; function findDefaultItem(schema) { const foundItem = schema?.find((item) => item?.default === true); return !hasVisibleFalse(foundItem?.value) ? foundItem?.value : undefined; @@ -59,15 +59,11 @@ export const DropDown = function DropDown({ } const setExposedItem = (value, index, onSelectFired = false) => { - setCurrentValue(value); + const selectedOptionLabel = index === undefined ? undefined : display_values?.[index]; + setInputValue(value, selectedOptionLabel); if (onSelectFired) { fireEvent('onSelect'); } - const exposedVariables = { - value, - selectedOptionLabel: index === undefined ? undefined : display_values?.[index], - }; - setExposedVariables(exposedVariables); }; function selectOption(value) { @@ -80,17 +76,6 @@ export const DropDown = function DropDown({ setExposedItem(undefined, undefined, true); } } - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('value', currentValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue]); useEffect(() => { if (isInitialRender.current) return; @@ -113,7 +98,7 @@ export const DropDown = function DropDown({ schema?.filter((item) => item?.visible)?.map((item) => item.label) ); if (hasVisibleFalse(currentValue)) { - setCurrentValue(findDefaultItem(schema)); + setInputValue(findDefaultItem(schema)); } } else setExposedVariable('optionLabels', display_values); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -137,9 +122,6 @@ export const DropDown = function DropDown({ }; setExposedVariables(exposedVariables); - if (hasVisibleFalse(currentValue)) { - setCurrentValue(findDefaultItem(schema)); - } isInitialRender.current = false; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -184,6 +166,13 @@ export const DropDown = function DropDown({ } }; + const setInputValue = (value, label) => { + setCurrentValue(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ value, isValid: validationStatus?.isValid, selectedOptionLabel: label }); + }; + const customStyles = { control: (provided, state) => ({ ...provided, @@ -276,10 +265,8 @@ export const DropDown = function DropDown({ onChange={(selectedOption, actionProps) => { setShowValidationError(true); if (actionProps.action === 'select-option') { - setCurrentValue(selectedOption.value); - setExposedVariable('value', selectedOption.value); + setInputValue(selectedOption.value, selectedOption.label); fireEvent('onSelect'); - setExposedVariable('selectedOptionLabel', selectedOption.label); } }} options={selectOptions} diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index a4b318eadf..1acddf4700 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -93,8 +93,8 @@ export const DropdownV2 = ({ const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; - const validationData = validate(currentValue); - const { isValid, validationError } = validationData; + const [validationStatus, setValidationStatus] = useState(validate(currentValue)); + const { isValid, validationError } = validationStatus; const ref = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState); @@ -132,7 +132,7 @@ export const DropdownV2 = ({ function selectOption(value) { const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value); if (val) { - setCurrentValue(value); + setInputValue(value); fireEvent('onSelect'); } } @@ -162,10 +162,22 @@ export const DropdownV2 = ({ } }; + const setInputValue = (value) => { + setCurrentValue(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + const _selectedOption = selectOptions.find((option) => option.value === value); + setExposedVariables({ + value, + isValid: validationStatus?.isValid, + selectedOption: pick(_selectedOption, ['label', 'value']), + }); + }; + useEffect(() => { if (advanced) { - setCurrentValue(findDefaultItem(schema)); - } else setCurrentValue(value); + setInputValue(findDefaultItem(schema)); + } else setInputValue(value); // eslint-disable-next-line react-hooks/exhaustive-deps }, [advanced, value, JSON.stringify(schema)]); @@ -186,18 +198,6 @@ export const DropdownV2 = ({ // Exposed variables - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('value', currentValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue]); - - useEffect(() => { - const _selectedOption = selectOptions.find((option) => option.value === currentValue); - setExposedVariable('selectedOption', pick(_selectedOption, ['label', 'value'])); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentValue, JSON.stringify(selectOptions)]); - useEffect(() => { if (isInitialRender.current) return; const _options = selectOptions?.map(({ label, value }) => ({ label, value })); @@ -221,11 +221,6 @@ export const DropdownV2 = ({ setExposedVariable('searchText', searchInputValue); }, [searchInputValue]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - }, [isValid]); - useEffect(() => { if (isInitialRender.current) return; setExposedVariable('isVisible', properties.visibility); @@ -250,7 +245,7 @@ export const DropdownV2 = ({ const _options = selectOptions?.map(({ label, value }) => ({ label, value })); const exposedVariables = { clear: async function () { - setCurrentValue(null); + setInputValue(null); }, setVisibility: async function (value) { setVisibility(value); @@ -448,10 +443,10 @@ export const DropdownV2 = ({ value={selectOptions.filter((option) => option.value === currentValue)[0] ?? null} onChange={(selectedOption, actionProps) => { if (actionProps.action === 'clear') { - setCurrentValue(null); + setInputValue(null); } if (actionProps.action === 'select-option') { - setCurrentValue(selectedOption.value); + setInputValue(selectedOption.value); fireEvent('onSelect'); } setIsFocused(false); diff --git a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx index a3795e8672..960955de57 100644 --- a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx +++ b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx @@ -65,8 +65,10 @@ export const MultiselectV2 = ({ const isMandatory = validation?.mandatory ?? false; const multiselectRef = React.useRef(null); const labelRef = React.useRef(null); - const validationData = validate(selected?.length ? selected?.map((option) => option.value) : null); - const { isValid, validationError } = validationData; + const [validationStatus, setValidationStatus] = useState( + validate(selected?.length ? selected?.map((option) => option.value) : null) + ); + const { isValid, validationError } = validationStatus; const valueContainerRef = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState); @@ -126,39 +128,32 @@ export const MultiselectV2 = ({ return false; } const onChangeHandler = (items, action) => { - setSelected(items); + setInputValue(items); if (action.action === 'select-option') { - setExposedVariable( - 'values', - items.map((item) => item.value) - ); fireEvent('onSelect'); } }; + useEffect(() => { let foundItem = findDefaultItem(values, advanced); - setSelected(foundItem); + setInputValue(foundItem); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectOptions]); useEffect(() => { let foundItem = findDefaultItem(values, advanced, true); - setSelected(foundItem); + setInputValue(foundItem); // eslint-disable-next-line react-hooks/exhaustive-deps }, [advanced, JSON.stringify(schema), JSON.stringify(values)]); useEffect(() => { if (isInitialRender.current) return; - setExposedVariable( - 'selectedOptions', - Array.isArray(selected) && selected?.map(({ label, value }) => ({ label, value })) - ); setExposedVariable( 'options', Array.isArray(selectOptions) && selectOptions?.map(({ label, value }) => ({ label, value })) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(selected), selectOptions]); + }, [selectOptions]); useEffect(() => { if (isInitialRender.current) return; @@ -186,14 +181,11 @@ export const MultiselectV2 = ({ }, [isMandatory]); useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - }, [isValid]); + const defaultItems = findDefaultItem(values, advanced, true); - useEffect(() => { const exposedVariables = { clear: async function () { - setSelected([]); + setInputValue([]); }, setVisibility: async function (value) { setVisibility(value); @@ -210,7 +202,7 @@ export const MultiselectV2 = ({ isDisabled: disabledState, isMandatory: isMandatory, isValid: isValid, - selectedOptions: Array.isArray(selected) && selected?.map(({ label, value }) => ({ label, value })), + selectedOptions: Array.isArray(defaultItems) && defaultItems?.map(({ label, value }) => ({ label, value })), options: Array.isArray(selectOptions) && selectOptions?.map(({ label, value }) => ({ label, value })), }; setExposedVariables(exposedVariables); @@ -237,7 +229,7 @@ export const MultiselectV2 = ({ newSelected.push(...optionsToAdd); } }); - setSelected(newSelected); + setInputValue(newSelected); } }); @@ -247,7 +239,7 @@ export const MultiselectV2 = ({ // Check if array provided is a list of objects with value key const _value = value.map((val) => (isObject(val) && has(val, 'value') ? val.value : val)); const newSelected = selected.filter((option) => !_value.includes(option.value)); - setSelected(newSelected); + setInputValue(newSelected); } }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -276,6 +268,17 @@ export const MultiselectV2 = ({ } }; + const setInputValue = (values) => { + setSelected(values); + const validationStatus = validate(values?.length ? values?.map((option) => option.value) : null); + setValidationStatus(validationStatus); + setExposedVariables({ + values: values.map((item) => item.value), + isValid: validationStatus?.isValid, + selectedOptions: Array.isArray(values) && values?.map(({ label, value }) => ({ label, value })), + }); + }; + useEffect(() => { document.addEventListener('mousedown', handleClickOutside, { capture: true }); return () => { @@ -485,7 +488,7 @@ export const MultiselectV2 = ({ showAllOption={showAllOption} isSelectAllSelected={isSelectAllSelected} setIsSelectAllSelected={setIsSelectAllSelected} - setSelected={setSelected} + setSelected={setInputValue} iconColor={iconColor} optionsLoadingState={optionsLoadingState && advanced} darkMode={darkMode} diff --git a/frontend/src/Editor/Components/NumberInput.jsx b/frontend/src/Editor/Components/NumberInput.jsx index f5c1e79956..a8cf3ac5c5 100644 --- a/frontend/src/Editor/Components/NumberInput.jsx +++ b/frontend/src/Editor/Components/NumberInput.jsx @@ -49,7 +49,8 @@ export const NumberInput = function NumberInput({ const [loading, setLoading] = useState(loadingState); const [showValidationError, setShowValidationError] = useState(false); const [value, setValue] = React.useState(Number(parseFloat(properties.value).toFixed(properties.decimalPlaces))); - const { isValid, validationError } = validate(value); + const [validationStatus, setValidationStatus] = useState(validate(value)); + const { isValid, validationError } = validationStatus; const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); @@ -65,31 +66,22 @@ export const NumberInput = function NumberInput({ }, [label]); useEffect(() => { - setValue(Number(parseFloat(value).toFixed(properties.decimalPlaces))); + setInputValue(Number(parseFloat(value).toFixed(properties.decimalPlaces))); // eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.decimalPlaces]); useEffect(() => { - setValue(Number(parseFloat(properties.value).toFixed(properties.decimalPlaces))); + setInputValue(Number(parseFloat(properties.value).toFixed(properties.decimalPlaces))); // eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.value]); const handleBlur = (e) => { - setValue(Number(parseFloat(e.target.value).toFixed(properties.decimalPlaces))); + setInputValue(Number(parseFloat(e.target.value).toFixed(properties.decimalPlaces))); setShowValidationError(true); e.stopPropagation(); fireEvent('onBlur'); setIsFocused(false); }; - - useEffect(() => { - if (isInitialRender.current) return; - if (!isNaN(value)) { - setExposedVariable('value', value); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); - useEffect(() => { if (isInitialRender.current) return; setExposedVariable('isMandatory', isMandatory); @@ -114,12 +106,6 @@ export const NumberInput = function NumberInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - useEffect(() => { disable !== disabledState && setDisable(disabledState); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -144,14 +130,12 @@ export const NumberInput = function NumberInput({ setText: async function (text) { if (text) { const newValue = Number(parseFloat(text)); - setValue(newValue); - setExposedVariable('value', text); + setInputValue(newValue); fireEvent('onChange'); } }, clear: async function () { - setValue(''); - setExposedVariable('value', ''); + setInputValue(''); fireEvent('onChange'); }, setLoading: async function (loading) { @@ -244,14 +228,13 @@ export const NumberInput = function NumberInput({ // eslint-disable-next-line import/namespace const handleChange = (e) => { - setValue(Number(parseFloat(e.target.value))); if (e.target.value == '') { - setValue(null); - setExposedVariable('value', null); + setInputValue(null); fireEvent('onChange'); + } else { + setInputValue(Number(parseFloat(e.target.value))); } if (!isNaN(Number(parseFloat(e.target.value)))) { - setExposedVariable('value', Number(parseFloat(e.target.value))); fireEvent('onChange'); } }; @@ -260,22 +243,27 @@ export const NumberInput = function NumberInput({ e.preventDefault(); // Prevent the default button behavior (form submission, page reload) const newValue = (value || 0) + 1; - setValue(newValue); + setInputValue(newValue); if (!isNaN(newValue)) { - setExposedVariable('value', newValue); fireEvent('onChange'); } }; const handleDecrement = (e) => { e.preventDefault(); const newValue = (value || 0) - 1; - setValue(newValue); + setInputValue(newValue); if (!isNaN(newValue)) { - setExposedVariable('value', newValue); fireEvent('onChange'); } }; + const setInputValue = (value) => { + setValue(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ ...(!isNaN(value) && { value }), isValid: validationStatus?.isValid }); + }; + const loaderStyle = { right: direction === 'right' && @@ -375,8 +363,7 @@ export const NumberInput = function NumberInput({ autoComplete="off" onKeyUp={(e) => { if (e.key === 'Enter') { - setValue(e.target.value); - setExposedVariable('value', e.target.value); + setInputValue(e.target.value); fireEvent('onEnterPressed'); } }} diff --git a/frontend/src/Editor/Components/PasswordInput.jsx b/frontend/src/Editor/Components/PasswordInput.jsx index 96d70365ad..4729de9101 100644 --- a/frontend/src/Editor/Components/PasswordInput.jsx +++ b/frontend/src/Editor/Components/PasswordInput.jsx @@ -48,7 +48,8 @@ export const PasswordInput = function PasswordInput({ const [disable, setDisable] = useState(disabledState || loadingState); const [passwordValue, setPasswordValue] = useState(properties.value); const [visibility, setVisibility] = useState(properties.visibility); - const { isValid, validationError } = validate(passwordValue); + const [validationStatus, setValidationStatus] = useState(validate(passwordValue)); + const { isValid, validationError } = validationStatus; const [showValidationError, setShowValidationError] = useState(false); const [labelWidth, setLabelWidth] = useState(0); const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side'; @@ -152,12 +153,6 @@ export const PasswordInput = function PasswordInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [loadingState]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - useEffect(() => { if (isInitialRender.current) return; setExposedVariable('isMandatory', isMandatory); @@ -184,8 +179,7 @@ export const PasswordInput = function PasswordInput({ useEffect(() => { if (isInitialRender.current) return; - setPasswordValue(properties.value); - setExposedVariable('value', properties?.value ?? ''); + setInputValue(properties?.value || ''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.value]); @@ -198,13 +192,11 @@ export const PasswordInput = function PasswordInput({ textInputRef.current.blur(); }, setText: async function (text) { - setPasswordValue(text); - setExposedVariable('value', text); + setInputValue(text); fireEvent('onChange'); }, clear: async function () { - setPasswordValue(''); - setExposedVariable('value', ''); + setInputValue(''); fireEvent('onChange'); }, setLoading: async function (loading) { @@ -228,7 +220,6 @@ export const PasswordInput = function PasswordInput({ value: properties?.value ?? '', }; - setPasswordValue(properties.value ?? ''); setExposedVariables(exposedVariables); isInitialRender.current = false; @@ -254,6 +245,14 @@ export const PasswordInput = function PasswordInput({ } return false; }); + + const setInputValue = (value) => { + setPasswordValue(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ value, isValid: validationStatus?.isValid }); + }; + const renderInput = () => ( <>
{ if (e.key === 'Enter') { - setPasswordValue(e.target.value); - setExposedVariable('value', e.target.value); + setInputValue(e.target.value); fireEvent('onEnterPressed'); } }} onChange={(e) => { - setPasswordValue(e.target.value); - setExposedVariable('value', e.target.value); + setInputValue(e.target.value); fireEvent('onChange'); }} onBlur={(e) => { diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index 7edbf89f60..f346589109 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.jsx @@ -47,8 +47,8 @@ export const TextInput = function TextInput({ const [disable, setDisable] = useState(disabledState || loadingState); const [value, setValue] = useState(properties.value); const [visibility, setVisibility] = useState(properties.visibility); - // const isValid = true; // TODO: remove this and uncomment the below line - const { isValid, validationError } = validate(value); + const [validationStatus, setValidationStatus] = useState(validate(value)); + const { isValid, validationError } = validationStatus; const [showValidationError, setShowValidationError] = useState(false); const [labelWidth, setLabelWidth] = useState(0); @@ -155,19 +155,12 @@ export const TextInput = function TextInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [loadingState]); - useEffect(() => { - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); - useEffect(() => { if (isInitialRender.current) return; if (properties.value === undefined) { - setValue(''); - setExposedVariable('value', ''); + setInputValue(''); } else { - setValue(properties.value); - setExposedVariable('value', properties.value); + setInputValue(properties.value); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -200,13 +193,11 @@ export const TextInput = function TextInput({ useEffect(() => { const exposedVariables = { setText: async function (text) { - setValue(text); - setExposedVariable('value', text); + setInputValue(text); fireEvent('onChange'); }, clear: async function () { - setValue(''); - setExposedVariable('value', ''); + setInputValue(''); fireEvent('onChange'); }, setFocus: async function () { @@ -241,13 +232,19 @@ export const TextInput = function TextInput({ isVisible: visibility, isDisabled: disable, }; - setValue(properties.value); setExposedVariables(exposedVariables); isInitialRender.current = false; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const setInputValue = (value) => { + setValue(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ value, isValid: validationStatus?.isValid }); + }; + const iconName = styles.icon; // Replace with the name of the icon you want // eslint-disable-next-line import/namespace const IconElement = Icons[iconName] == undefined ? Icons['IconHome2'] : Icons[iconName]; @@ -320,14 +317,12 @@ export const TextInput = function TextInput({ } validation-without-icon`} onKeyUp={(e) => { if (e.key === 'Enter') { - setValue(e.target.value); - setExposedVariable('value', e.target.value); + setInputValue(e.target.value); fireEvent('onEnterPressed'); } }} onChange={(e) => { - setValue(e.target.value); - setExposedVariable('value', e.target.value); + setInputValue(e.target.value); fireEvent('onChange'); }} onBlur={(e) => { diff --git a/frontend/src/Editor/Components/ToggleV2.jsx b/frontend/src/Editor/Components/ToggleV2.jsx index 11569828c7..752d14a361 100644 --- a/frontend/src/Editor/Components/ToggleV2.jsx +++ b/frontend/src/Editor/Components/ToggleV2.jsx @@ -105,7 +105,8 @@ export const ToggleSwitchV2 = ({ const [on, setOn] = useState(Boolean(defaultValue)); const label = properties.label; const isMandatory = validation?.mandatory ?? false; - const { isValid, validationError } = validate(on); + const [validationStatus, setValidationStatus] = useState(validate(on)); + const { isValid, validationError } = validationStatus; const [showValidationError, setShowValidationError] = useState(true); const [loading, setLoading] = useState(properties?.loadingState); const [disable, setDisable] = useState(properties.disabledState || properties.loadingState); @@ -123,17 +124,23 @@ export const ToggleSwitchV2 = ({ }; // Exposing the initially set false value once on load + const setInputValue = (value) => { + setOn(value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariables({ value, isValid: validationStatus?.isValid }); + }; + useEffect(() => { if (isInitialRender.current) return; - setExposedVariable('value', defaultValue); // eslint-disable-next-line react-hooks/exhaustive-deps - setOn(defaultValue); + setInputValue(defaultValue); // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]); const toggle = () => { - setOn(!on); + setInputValue(!on); setUserInteracted(true); }; @@ -180,17 +187,11 @@ export const ToggleSwitchV2 = ({ setExposedVariable('isDisabled', disable); // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); - useEffect(() => { - if (isInitialRender.current) return; - setExposedVariable('isValid', isValid); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isValid]); useEffect(() => { const exposedVariables = { setValue: async function (value) { - setOn(value); - setExposedVariable('value', value); + setInputValue(value); setUserInteracted(true); }, setVisibility: async function (state) { @@ -214,15 +215,13 @@ export const ToggleSwitchV2 = ({ value: defaultValue, }; setExposedVariables(exposedVariables); - setOn(defaultValue); isInitialRender.current = false; }, []); useEffect(() => { setExposedVariable('toggle', async function () { - setExposedVariable('value', !on); + setInputValue(!on); fireEvent('onChange'); - setOn(!on); setUserInteracted(true); }); // eslint-disable-next-line react-hooks/exhaustive-deps From 4b0a640b2893cad60d657321dcec875e24d9e7f6 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 02:42:58 +0530 Subject: [PATCH 005/104] Fixed checkbox toggle issue --- frontend/src/Editor/Components/Checkbox.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/Editor/Components/Checkbox.jsx b/frontend/src/Editor/Components/Checkbox.jsx index 3a9a8c1ebb..54ad39d2c6 100644 --- a/frontend/src/Editor/Components/Checkbox.jsx +++ b/frontend/src/Editor/Components/Checkbox.jsx @@ -98,6 +98,16 @@ export const Checkbox = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('toggle', async function () { + setInputValue(!checked); + fireEvent('onChange'); + setUserInteracted(true); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [checked]); + useEffect(() => { const setCheckedAndNotify = async (status) => { setInputValue(status); @@ -125,12 +135,9 @@ export const Checkbox = ({ setExposedVariable('isDisabled', disable); }, toggle: () => { - setExposedVariable('toggle', async function () { - setInputValue(!checked); - fireEvent('onChange'); - setUserInteracted(true); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps + setInputValue(!checked); + fireEvent('onChange'); + setUserInteracted(true); }, label: label, isMandatory: isMandatory, From c1f76a58ad7ca21ba22cf83c1a0fd72988d78c8b Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 14:42:07 +0530 Subject: [PATCH 006/104] Fixed issue causing table columns to autogenerate on deleting them --- frontend/src/AppBuilder/Widgets/Table/Table.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index d6966192cb..559ca42267 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -581,7 +581,7 @@ export const Table = React.memo( ); useDynamicColumn && setGeneratedColumn(generatedColumnFromData); } - }, [tableData, JSON.stringify(dynamicColumn)]); + }, [JSON.stringify(tableData), JSON.stringify(dynamicColumn)]); const computedStyles = { // width: `${width}px`, From ef402992f3ced1124e21d9b391a5caba3feb9c27 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Fri, 1 Nov 2024 02:39:01 +0530 Subject: [PATCH 007/104] Fixed string & number as currentTab causing issues --- frontend/src/AppBuilder/Widgets/Tabs.jsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Tabs.jsx b/frontend/src/AppBuilder/Widgets/Tabs.jsx index 5a78a07809..3a93fa698b 100644 --- a/frontend/src/AppBuilder/Widgets/Tabs.jsx +++ b/frontend/src/AppBuilder/Widgets/Tabs.jsx @@ -69,14 +69,14 @@ export const Tabs = function Tabs({ }, [parsedDefaultTab]); useEffect(() => { - const currentTabData = parsedTabs.filter((tab) => tab.id === currentTab); + const currentTabData = parsedTabs.filter((tab) => tab.id == currentTab); setBgColor(currentTabData[0]?.backgroundColor ? currentTabData[0]?.backgroundColor : darkMode ? '#324156' : '#fff'); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTab, darkMode]); function computeTabDisplay(componentId, id) { let tabVisibility = 'none'; - if (id !== currentTab) { + if (id != currentTab) { return tabVisibility; } @@ -87,14 +87,13 @@ export const Tabs = function Tabs({ } } - return id === currentTab ? 'block' : 'none'; + return id == currentTab ? 'block' : 'none'; } useEffect(() => { const exposedVariables = { setTab: async function (id) { - id = typeof id === 'number' ? String(id) : id; - if (id && currentTab !== id) { + if (currentTab != id) { setCurrentTab(id); setExposedVariable('currentTab', id); fireEvent('onTabSwitch'); @@ -134,7 +133,7 @@ export const Tabs = function Tabs({ function shouldRenderTabContent(tab) { if (parsedRenderOnlyActiveTab) { - return tab.id === currentTab; + return tab.id == currentTab; } return true; // Render by default if no specific conditions are met } @@ -162,7 +161,7 @@ export const Tabs = function Tabs({ className="nav-item" style={{ opacity: tab?.disabled && '0.5', width: tabWidth == 'split' && equalSplitWidth + '%' }} onClick={() => { - if (currentTab === tab.id) return; + if (currentTab == tab.id) return; !tab?.disabled && setCurrentTab(tab.id); !tab?.disabled && setExposedVariable('currentTab', tab.id); @@ -193,7 +192,7 @@ export const Tabs = function Tabs({
{ - if (currentTab === tab.id) { + if (currentTab == tab.id) { parentRef.current = newCurrent; } }} @@ -202,7 +201,7 @@ export const Tabs = function Tabs({ > {shouldRenderTabContent(tab) && renderTabContent(id, tab)} - {/* {tab.id === currentTab && } */} + {/* {tab.id == currentTab && } */}
))}
From 65b4be0aac35235e0ac2d06b7221e69af4091a9d Mon Sep 17 00:00:00 2001 From: Kartik Gupta Date: Mon, 4 Nov 2024 10:26:29 +0530 Subject: [PATCH 008/104] fix table going in infinite loop when using dynamic columns without id (#2527) --- frontend/src/AppBuilder/Widgets/Table/Table.jsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 559ca42267..8bbe06ffd6 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -579,7 +579,22 @@ export const Table = React.memo( properties.autogenerateColumns ?? false, id ); - useDynamicColumn && setGeneratedColumn(generatedColumnFromData); + if (useDynamicColumn) { + const dynamicColumnHasId = dynamicColumn && dynamicColumn.every((column) => 'id' in column); + if (!dynamicColumnHasId) { + // if dynamic columns do not have an id then we need to manually compare the generated columns with the columns in the state because the id that we generate for columns without id is a uuid and it will be different every time + const generatedColumnsWithoutIds = generatedColumnFromData.map(({ id, ...rest }) => ({ + ...rest, + })); + const columnsFromStateWithoutIds = generatedColumn.map(({ id, ...rest }) => ({ + ...rest, + })); + !isEqual(generatedColumnsWithoutIds, columnsFromStateWithoutIds) && + setGeneratedColumn(generatedColumnFromData); + return; + } + setGeneratedColumn(generatedColumnFromData); + } } }, [JSON.stringify(tableData), JSON.stringify(dynamicColumn)]); From b12aecb5ad5585a2d19c834041f019f04b6fc3c0 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 02:10:48 +0530 Subject: [PATCH 009/104] Fixed undo redo breaking on setting component properties --- frontend/src/AppBuilder/Widgets/Table/Table.jsx | 5 ++++- .../Widgets/Table/columns/autogenerateColumns.js | 12 ++++++++++-- .../src/AppBuilder/_stores/slices/undoRedoSlice.js | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 8bbe06ffd6..714f2dd640 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -139,6 +139,7 @@ export const Table = React.memo( const updatedDataReference = useRef([]); const preSelectRow = useRef(false); const initialPageCountRef = useRef(null); + const isInitialRender = useRef(true); const allAppEvents = useEvents(); // const { events: allAppEvents } = useAppInfo(); const tableEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id); @@ -577,8 +578,10 @@ export const Table = React.memo( dynamicColumn, setComponentProperty, properties.autogenerateColumns ?? false, - id + id, + isInitialRender.current ); + isInitialRender.current = false; if (useDynamicColumn) { const dynamicColumnHasId = dynamicColumn && dynamicColumn.every((column) => 'id' in column); if (!dynamicColumnHasId) { diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js index 235205dbb3..e093bdeac5 100644 --- a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js +++ b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js @@ -9,7 +9,8 @@ export default function autogenerateColumns( dynamicColumn = [], setProperty, generateNestedColumns, - id + id, + isInitialRender ) { if (useDynamicColumn) { if (dynamicColumn.length > 0 && dynamicColumn[0].name) { @@ -107,7 +108,14 @@ export default function autogenerateColumns( finalKeys.includes(column?.key || column?.name) ); - setTimeout(() => setProperty(id, 'columns', finalColumns, 'properties'), 10); + setTimeout( + () => + setProperty(id, 'columns', finalColumns, 'properties', undefined, undefined, undefined, { + skipUndoRedo: isInitialRender, + saveAfterAction: true, + }), + 10 + ); } const dataTypeToColumnTypeMapping = { diff --git a/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js b/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js index ec8e9bbf58..901baed1f3 100644 --- a/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/undoRedoSlice.js @@ -106,6 +106,7 @@ export const createUndoRedoSlice = (set, get) => { componenetPropertiesToUpdate.paramType, componenetPropertiesToUpdate.attr, undefined, + undefined, { skipUndoRedo: true } ); } From dd1620a3bd1ef21c570d98cedc9dc72ddc8c12ee Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 02:15:00 +0530 Subject: [PATCH 010/104] Fixed issue causing entire subcontainer components to not be undoed/redoed together --- .../_stores/slices/componentsSlice.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index d5b0afa9d4..3f36c005f7 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -743,9 +743,12 @@ export const createComponentsSlice = (set, get) => ({ deleteComponentNameIdMapping(oldName, moduleId); } updateComponentDependencyGraph(moduleId, newComponent); - const parentId = newComponent.component.parent || 'canvas'; - set( - withUndoRedo((state) => { + }); + set( + withUndoRedo((state) => { + newComponents.forEach((newComponent) => { + const parentId = newComponent.component.parent || 'canvas'; + if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; } @@ -754,15 +757,14 @@ export const createComponentsSlice = (set, get) => ({ } const page = state.modules[moduleId].pages[state.currentPageIndex]; page.components[newComponent.id] = newComponent; - }, skipUndoRedo), - false, - 'addComponentToCurrentPage' - ); - if (index === 0) { - //incase of multiple components, only first one will be selected since it will be the parent component - get().setSelectedComponents([newComponent.id]); - } - }); + }); + }, skipUndoRedo), + false, + 'addComponentToCurrentPage' + ); + + //incase of multiple components, only first one will be selected since it will be the parent component + get().setSelectedComponents([newComponents[0].id]); if (saveAfterAction) { saveComponentChanges(diff, 'components', 'create') From 8515d226bc2c1ba8d825219e940ad148b0bce72d Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 16:20:00 +0530 Subject: [PATCH 011/104] Minor fixes for table autogenerate column --- frontend/src/AppBuilder/Widgets/Table/Table.jsx | 5 +---- .../AppBuilder/Widgets/Table/columns/autogenerateColumns.js | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 714f2dd640..8bbe06ffd6 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -139,7 +139,6 @@ export const Table = React.memo( const updatedDataReference = useRef([]); const preSelectRow = useRef(false); const initialPageCountRef = useRef(null); - const isInitialRender = useRef(true); const allAppEvents = useEvents(); // const { events: allAppEvents } = useAppInfo(); const tableEvents = allAppEvents.filter((event) => event.target === 'component' && event.sourceId === id); @@ -578,10 +577,8 @@ export const Table = React.memo( dynamicColumn, setComponentProperty, properties.autogenerateColumns ?? false, - id, - isInitialRender.current + id ); - isInitialRender.current = false; if (useDynamicColumn) { const dynamicColumnHasId = dynamicColumn && dynamicColumn.every((column) => 'id' in column); if (!dynamicColumnHasId) { diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js index e093bdeac5..334f115d4e 100644 --- a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js +++ b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js @@ -9,8 +9,7 @@ export default function autogenerateColumns( dynamicColumn = [], setProperty, generateNestedColumns, - id, - isInitialRender + id ) { if (useDynamicColumn) { if (dynamicColumn.length > 0 && dynamicColumn[0].name) { @@ -111,7 +110,7 @@ export default function autogenerateColumns( setTimeout( () => setProperty(id, 'columns', finalColumns, 'properties', undefined, undefined, undefined, { - skipUndoRedo: isInitialRender, + skipUndoRedo: true, saveAfterAction: true, }), 10 From 43618a2186824e911fd71f121e6089f507f4895c Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 6 Nov 2024 16:38:50 +0530 Subject: [PATCH 012/104] Missing changes added --- .../src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js index 334f115d4e..98faa12da0 100644 --- a/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js +++ b/frontend/src/AppBuilder/Widgets/Table/columns/autogenerateColumns.js @@ -109,7 +109,7 @@ export default function autogenerateColumns( setTimeout( () => - setProperty(id, 'columns', finalColumns, 'properties', undefined, undefined, undefined, { + setProperty(id, 'columns', finalColumns, 'properties', 'value', false, 'canvas', { skipUndoRedo: true, saveAfterAction: true, }), From e81a0b1be961240859414b5244dccb2f3336022a Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Thu, 7 Nov 2024 14:08:58 +0530 Subject: [PATCH 013/104] Fixed listview breaking --- .../_stores/slices/componentsSlice.js | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 3f36c005f7..d5b0afa9d4 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -743,12 +743,9 @@ export const createComponentsSlice = (set, get) => ({ deleteComponentNameIdMapping(oldName, moduleId); } updateComponentDependencyGraph(moduleId, newComponent); - }); - set( - withUndoRedo((state) => { - newComponents.forEach((newComponent) => { - const parentId = newComponent.component.parent || 'canvas'; - + const parentId = newComponent.component.parent || 'canvas'; + set( + withUndoRedo((state) => { if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; } @@ -757,14 +754,15 @@ export const createComponentsSlice = (set, get) => ({ } const page = state.modules[moduleId].pages[state.currentPageIndex]; page.components[newComponent.id] = newComponent; - }); - }, skipUndoRedo), - false, - 'addComponentToCurrentPage' - ); - - //incase of multiple components, only first one will be selected since it will be the parent component - get().setSelectedComponents([newComponents[0].id]); + }, skipUndoRedo), + false, + 'addComponentToCurrentPage' + ); + if (index === 0) { + //incase of multiple components, only first one will be selected since it will be the parent component + get().setSelectedComponents([newComponent.id]); + } + }); if (saveAfterAction) { saveComponentChanges(diff, 'components', 'create') From 220beec2a2d671326f93b4d44967b9b4209418b5 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Thu, 7 Nov 2024 14:53:17 +0530 Subject: [PATCH 014/104] Fixed button group unselect and some css --- .../src/AppBuilder/WidgetManager/widgets/buttonGroup.js | 4 ++-- frontend/src/Editor/Components/ButtonGroup.jsx | 9 ++++++++- server/src/helpers/widget-config/buttonGroup.js | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js index 65b7e77807..c0fa889dd5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js @@ -146,8 +146,8 @@ export const buttonGroupConfig = { visibility: { value: '{{true}}' }, borderRadius: { value: '{{4}}' }, disabledState: { value: '{{false}}' }, - selectedTextColor: { value: '' }, - selectedBackgroundColor: { value: '' }, + selectedTextColor: { value: '#FFFFFF' }, + selectedBackgroundColor: { value: '#4368E3' }, }, }, }; diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx index 9711820bc7..7364348271 100644 --- a/frontend/src/Editor/Components/ButtonGroup.jsx +++ b/frontend/src/Editor/Components/ButtonGroup.jsx @@ -34,6 +34,12 @@ export const ButtonGroup = function Button({ display: visibility ? '' : 'none', }; + const disabledStyles = { + opacity: 0.5, + pointerEvents: 'none', + cursor: 'not-allowed', + }; + const [defaultActive, setDefaultActive] = useState(defaultSelected); const [data, setData] = useState(values); @@ -62,7 +68,7 @@ export const ButtonGroup = function Button({ const buttonClick = (index) => { if (defaultActive?.includes(values[index]) && multiSelection) { - const copyDefaultActive = defaultActive; + const copyDefaultActive = [...defaultActive]; copyDefaultActive?.splice(copyDefaultActive?.indexOf(values[index]), 1); setDefaultActive(copyDefaultActive); setExposedVariable('selected', copyDefaultActive.join(',')); @@ -100,6 +106,7 @@ export const ButtonGroup = function Button({ color: defaultActive?.includes(values[index]) ? selectedTextColor : textColor, transition: 'all .1s ease', boxShadow, + ...(disabledState && disabledStyles), }} key={index} disabled={disabledState} diff --git a/server/src/helpers/widget-config/buttonGroup.js b/server/src/helpers/widget-config/buttonGroup.js index 65b7e77807..c0fa889dd5 100644 --- a/server/src/helpers/widget-config/buttonGroup.js +++ b/server/src/helpers/widget-config/buttonGroup.js @@ -146,8 +146,8 @@ export const buttonGroupConfig = { visibility: { value: '{{true}}' }, borderRadius: { value: '{{4}}' }, disabledState: { value: '{{false}}' }, - selectedTextColor: { value: '' }, - selectedBackgroundColor: { value: '' }, + selectedTextColor: { value: '#FFFFFF' }, + selectedBackgroundColor: { value: '#4368E3' }, }, }, }; From 6dda1d25830e3a20c3442f532add7630b3aa549f Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Wed, 30 Oct 2024 15:00:31 +0530 Subject: [PATCH 015/104] Fixed form breaking on wrong input --- frontend/src/AppBuilder/Widgets/Form/FormUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js index bf4a8cd647..8b9c345c13 100644 --- a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js +++ b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js @@ -77,6 +77,7 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') { const itemType = typeResolver(value?.type); if (itemType) { uiComponentsDraft.push(getComponentDefinition('Text')); + uiComponentsDraft.push(getComponentDefinition(itemType)); //only add if there is a valid item type } else { // useCurrentStateStore.getState().actions.setErrors({ @@ -89,8 +90,8 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') { // }, // }); uiComponentsDraft.push(undefined); + uiComponentsDraft.push(undefined); } - uiComponentsDraft.push(getComponentDefinition(itemType)); }); Object.entries(JSONSchema?.properties).forEach(([key, value], index) => { if (uiComponentsDraft?.length > 0 && uiComponentsDraft[index * 2 + 1]) { From b6822a3ce290acc0af87d67d14e3762955270650 Mon Sep 17 00:00:00 2001 From: namanmathur372 <131517641+namanmathur372@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:35:07 +0530 Subject: [PATCH 016/104] Integrate Pinecone Plugin with Core Operations for Advanced Vector Management (#11188) * Initialize ToolJet plugin with default files and installed dependencies Created the folder structure and default files for the pinecone plugin the server/src/assets/marketplace/plugins.json file also got updated automatically and added an entry for the Pinecone plugin. Installed the specified versions of @grpc/grpc-js@^1.12.0 and @pinecone-database/pinecone@^3.0.3 packages into the project. * Update Pinecone plugin manifest with API key authentication Updated Pinecone plugin manifest to include API key authentication as an encrypted string. Added API key field with description and help text for generating the key via the Pinecone Console. Marked apiKey as a required field in the schema. * Enhance Pinecone operations.json with vector management options Amended operations.json to support various vector management operations for the Pinecone plugin. Added new fields for index, operation, and specific properties for operations like get_index_stats, list_vector_ids, fetch_vectors, upsert_vectors, update_vector, and delete_vectors. Introduced support for vector ID management, metadata, namespace, and filter options in JSON format. Updated the schema type from api to database to better reflect the plugin's functionality. * Add query operations and enhance types for Pinecone plugin Created query_operations.ts to handle Pinecone operations: getIndexStats, listVectorIds, fetchVectors, upsertVectors, updateVector, and deleteVectors. Added error handling, namespace support, and consolidated vector deletion logic. Updated types.ts with detailed SourceOptions and QueryOptions, including fields like index, ids, vectors, and pagination. Introduced types for Vector, SparseValues, and an enum for available operations, ensuring type safety. Functions for getIndexStats and listVectorIds work correctly, but other operations are not functioning as expected. * Implement Pinecone service with query operations and connection handling Updated index.ts with PineconeService to handle operations: getIndexStats, listVectorIds, fetchVectors, upsertVectors, updateVector, and deleteVectors. Added connection testing and API key handling functions. Error handling for invalid operations included. Only getIndexStats and listVectorIds are working correctly; other operations need debugging. * Fix operations and add query vector operation * Update placeholders for boolean fields Set placeholders for delete_all, include_values, and include_metadata to indicate "true (false by default)" for clearer defaults. --------- Co-authored-by: parthy007 --- frontend/package-lock.json | 1 + marketplace/package-lock.json | 228 ++++++++++++-- marketplace/plugins/pinecone/.gitignore | 5 + marketplace/plugins/pinecone/README.md | 4 + .../plugins/pinecone/__tests__/index.js | 7 + marketplace/plugins/pinecone/lib/icon.svg | 72 +++++ marketplace/plugins/pinecone/lib/index.ts | 91 ++++++ .../plugins/pinecone/lib/manifest.json | 33 ++ .../plugins/pinecone/lib/operations.json | 294 ++++++++++++++++++ .../plugins/pinecone/lib/query_operations.ts | 181 +++++++++++ marketplace/plugins/pinecone/lib/types.ts | 44 +++ marketplace/plugins/pinecone/package.json | 28 ++ marketplace/plugins/pinecone/tsconfig.json | 11 + server/src/assets/marketplace/plugins.json | 18 +- 14 files changed, 997 insertions(+), 20 deletions(-) create mode 100644 marketplace/plugins/pinecone/.gitignore create mode 100644 marketplace/plugins/pinecone/README.md create mode 100644 marketplace/plugins/pinecone/__tests__/index.js create mode 100644 marketplace/plugins/pinecone/lib/icon.svg create mode 100644 marketplace/plugins/pinecone/lib/index.ts create mode 100644 marketplace/plugins/pinecone/lib/manifest.json create mode 100644 marketplace/plugins/pinecone/lib/operations.json create mode 100644 marketplace/plugins/pinecone/lib/query_operations.ts create mode 100644 marketplace/plugins/pinecone/lib/types.ts create mode 100644 marketplace/plugins/pinecone/package.json create mode 100644 marketplace/plugins/pinecone/tsconfig.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ce64533ece..95c08b66cf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -215,6 +215,7 @@ "@tooljet-plugins/graphql": "file:packages/graphql", "@tooljet-plugins/grpc": "file:packages/grpc", "@tooljet-plugins/influxdb": "file:packages/influxdb", + "@tooljet-plugins/jira": "file:packages/jira", "@tooljet-plugins/mailgun": "file:packages/mailgun", "@tooljet-plugins/mariadb": "file:packages/mariadb", "@tooljet-plugins/minio": "file:packages/minio", diff --git a/marketplace/package-lock.json b/marketplace/package-lock.json index 701d0139e6..b59f40959d 100644 --- a/marketplace/package-lock.json +++ b/marketplace/package-lock.json @@ -4907,6 +4907,73 @@ "dev": true, "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", + "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "dev": true, @@ -5572,6 +5639,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@lerna/child-process": { "version": "6.6.2", "dev": true, @@ -7415,6 +7491,17 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pinecone-database/pinecone": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@pinecone-database/pinecone/-/pinecone-3.0.3.tgz", + "integrity": "sha512-0cAG0d/6knVZgVyXM1II4qG3dyOepLuAQsCXTOJomdA7iQxf+/Om9mq9Cw4QObr56oZ+lqtptlw5qz0BQaBX2Q==", + "dependencies": { + "encoding": "^0.1.13" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "dev": true, @@ -7428,6 +7515,60 @@ "version": "1.0.0", "license": "Apache-2.0" }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sigstore/bundle": { "version": "1.1.0", "dev": true, @@ -8305,10 +8446,6 @@ "resolved": "plugins/engagespot", "link": true }, - "node_modules/@tooljet-marketplace/gemini": { - "resolved": "plugins/gemini", - "link": true - }, "node_modules/@tooljet-marketplace/github": { "resolved": "plugins/github", "link": true @@ -8321,6 +8458,10 @@ "resolved": "plugins/openai", "link": true }, + "node_modules/@tooljet-marketplace/pinecone": { + "resolved": "plugins/pinecone", + "link": true + }, "node_modules/@tooljet-marketplace/plivo": { "resolved": "plugins/plivo", "link": true @@ -8329,6 +8470,10 @@ "resolved": "plugins/pocketbase", "link": true }, + "node_modules/@tooljet-marketplace/portkey": { + "resolved": "plugins/portkey", + "link": true + }, "node_modules/@tooljet-marketplace/presto": { "resolved": "plugins/presto", "link": true @@ -10376,13 +10521,6 @@ "dev": true, "license": "MIT" }, - "node_modules/depd": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/deprecation": { "version": "2.3.1", "dev": true, @@ -10525,7 +10663,6 @@ "node_modules/encoding": { "version": "0.1.13", "license": "MIT", - "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -10533,7 +10670,6 @@ "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "license": "MIT", - "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -10596,7 +10732,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, "engines": { "node": ">=6" } @@ -11309,7 +11444,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -14177,6 +14311,11 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "license": "MIT" @@ -14235,6 +14374,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -16817,6 +16961,29 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocols": { "version": "2.0.1", "dev": true, @@ -17296,7 +17463,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18768,7 +18934,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -18926,7 +19091,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -19025,6 +19189,7 @@ "plugins/gemini": { "name": "@tooljet-marketplace/gemini", "version": "1.0.0", + "extraneous": true, "dependencies": { "@tooljet-marketplace/common": "^1.0.0", "portkey-ai": "^1.3.1" @@ -19079,6 +19244,19 @@ "typescript": "^4.7.4" } }, + "plugins/pinecone": { + "name": "@tooljet-marketplace/pinecone", + "version": "1.0.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.2", + "@pinecone-database/pinecone": "^3.0.3", + "@tooljet-marketplace/common": "^1.0.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.34.0", + "typescript": "^4.7.4" + } + }, "plugins/plivo": { "name": "@tooljet-marketplace/plivo", "version": "1.0.0", @@ -19103,6 +19281,18 @@ "typescript": "^4.7.4" } }, + "plugins/portkey": { + "name": "@tooljet-marketplace/portkey", + "version": "1.0.0", + "dependencies": { + "@tooljet-marketplace/common": "^1.0.0", + "portkey-ai": "^1.3.1" + }, + "devDependencies": { + "@vercel/ncc": "^0.34.0", + "typescript": "^4.7.4" + } + }, "plugins/presto": { "name": "@tooljet-marketplace/presto", "version": "1.0.0", @@ -19176,4 +19366,4 @@ } } } -} \ No newline at end of file +} diff --git a/marketplace/plugins/pinecone/.gitignore b/marketplace/plugins/pinecone/.gitignore new file mode 100644 index 0000000000..23e6609462 --- /dev/null +++ b/marketplace/plugins/pinecone/.gitignore @@ -0,0 +1,5 @@ +node_modules +lib/*.d.* +lib/*.js +lib/*.js.map +dist/* \ No newline at end of file diff --git a/marketplace/plugins/pinecone/README.md b/marketplace/plugins/pinecone/README.md new file mode 100644 index 0000000000..0feecc5e21 --- /dev/null +++ b/marketplace/plugins/pinecone/README.md @@ -0,0 +1,4 @@ + +# Pinecone + +Documentation on: https://docs.tooljet.com/docs/data-sources/pinecone \ No newline at end of file diff --git a/marketplace/plugins/pinecone/__tests__/index.js b/marketplace/plugins/pinecone/__tests__/index.js new file mode 100644 index 0000000000..056582a50e --- /dev/null +++ b/marketplace/plugins/pinecone/__tests__/index.js @@ -0,0 +1,7 @@ +'use strict'; + +const pinecone = require('../lib'); + +describe('pinecone', () => { + it.todo('needs tests'); +}); diff --git a/marketplace/plugins/pinecone/lib/icon.svg b/marketplace/plugins/pinecone/lib/icon.svg new file mode 100644 index 0000000000..2bddce89fd --- /dev/null +++ b/marketplace/plugins/pinecone/lib/icon.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/marketplace/plugins/pinecone/lib/index.ts b/marketplace/plugins/pinecone/lib/index.ts new file mode 100644 index 0000000000..a3eaa07e0b --- /dev/null +++ b/marketplace/plugins/pinecone/lib/index.ts @@ -0,0 +1,91 @@ +import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common'; +import { SourceOptions, QueryOptions, Operation } from './types'; +import { + getIndexStats, + listVectorIds, + fetchVectors, + upsertVectors, + updateVector, + deleteVectors, + quertVectors, +} from './query_operations'; +import { Pinecone } from '@pinecone-database/pinecone'; + +export default class PineconeService implements QueryService { + // Function to run the specified operation + async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise { + const operation = queryOptions.operation; + const pinecone = await this.getConnection(sourceOptions); + let result = {}; + + try { + switch (operation) { + case Operation.GetIndexStats: + result = await getIndexStats(pinecone, queryOptions); + break; + case Operation.ListVectorIds: + result = await listVectorIds(pinecone, queryOptions); + break; + case Operation.FetchVectors: + result = await fetchVectors(pinecone, queryOptions); + break; + case Operation.UpsertVectors: + result = await upsertVectors(pinecone, queryOptions); + break; + case Operation.UpdateVector: + result = await updateVector(pinecone, queryOptions); + break; + case Operation.DeleteVectors: + result = await deleteVectors(pinecone, queryOptions); + break; + case Operation.QueryVectors: + result = await quertVectors(pinecone, queryOptions); + break; + default: + throw new QueryError('Query could not be completed', 'Invalid operation', {}); + } + } catch (error) { + throw new QueryError('Query could not be completed', error?.message, {}); + } + + return { + status: 'ok', + data: result, + }; + } + + // Function to test the Pinecone connection + async testConnection(sourceOptions: SourceOptions): Promise { + const pinecone = await this.getConnection(sourceOptions); + + try { + const indexes = await pinecone.listIndexes(); + console.log('Indexes fetched:', indexes); + + if (indexes.indexes.length > 0) { + console.log('Connection successful, indexes found'); + return { status: 'ok' }; + } else { + console.error('No indexes found'); + throw new QueryError('No indexes found', 'The index list is empty', {}); + } + } catch (error) { + console.error('Connection could not be established:', error.message); + throw new QueryError('Connection could not be established', error?.message, {}); + } + } + + async getConnection(sourceOptions: SourceOptions): Promise { + const { apiKey } = sourceOptions; + + if (!apiKey) { + throw new QueryError('API key missing', 'No API key provided in source options', {}); + } + + const pinecone = new Pinecone({ + apiKey: apiKey, + }); + + return pinecone; + } +} diff --git a/marketplace/plugins/pinecone/lib/manifest.json b/marketplace/plugins/pinecone/lib/manifest.json new file mode 100644 index 0000000000..afe92e821d --- /dev/null +++ b/marketplace/plugins/pinecone/lib/manifest.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json", + "title": "Pinecone Plugin", + "description": "A schema defining Pinecone datasource", + "type": "api", + "source": { + "name": "Pinecone", + "kind": "pinecone", + "exposedVariables": { + "isLoading": false, + "data": {}, + "rawData": {} + }, + "options": { + "apiKey": { + "type": "string", + "encrypted": true + } + } + }, + "defaults": {}, + "properties": { + "apiKey": { + "label": "API Key", + "key": "apiKey", + "type": "password", + "description": "Enter your Pinecone API Key" + } + }, + "required": [ + "apiKey" + ] +} diff --git a/marketplace/plugins/pinecone/lib/operations.json b/marketplace/plugins/pinecone/lib/operations.json new file mode 100644 index 0000000000..b5d3a4e3c9 --- /dev/null +++ b/marketplace/plugins/pinecone/lib/operations.json @@ -0,0 +1,294 @@ +{ + "$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json", + "title": "Pinecone Datasource", + "description": "A schema defining Pinecone datasource", + "type": "database", + "defaults": {}, + "properties": { + "index": { + "label": "Index", + "key": "index", + "type": "text", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "operation": { + "label": "Operation", + "key": "operation", + "type": "dropdown-component-flip", + "description": "Select an operation", + "list": [ + { "value": "get_index_stats", "name": "Get Index Stats" }, + { "value": "list_vector_ids", "name": "List Vector IDs" }, + { "value": "fetch_vectors", "name": "Fetch Vectors" }, + { "value": "upsert_vectors", "name": "Upsert Vectors" }, + { "value": "update_vector", "name": "Update a Vector" }, + { "value": "delete_vectors", "name": "Delete Vectors" }, + { "value": "query_vector", "name": "Query Vectors" } + ] + }, + "get_index_stats": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + } + }, + "list_vector_ids": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "prefix": { + "label": "Prefix", + "key": "prefix", + "type": "codehinter", + "description": "Enter a prefix to filter vector IDs", + "placeholder": "document1#", + "height": "36px" + }, + "limit": { + "label": "Limit", + "key": "limit", + "type": "codehinter", + "description": "Enter the maximum number of vector IDs to return", + "placeholder": "100", + "height": "36px" + }, + "paginationToken": { + "label": "Pagination Token", + "key": "paginationToken", + "type": "codehinter", + "description": "Enter next token for next set of vector IDs", + "placeholder": "Tm90aGluzYB0byBZzWUGaGVyZQo=", + "height": "36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + } + }, + "fetch_vectors": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "ids": { + "label": "IDs", + "key": "ids", + "type": "codehinter", + "description": "Enter vector IDs as JSON array", + "placeholder": "[\"id-1\", \"id-2\"]", + "height": "36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + } + }, + "upsert_vectors": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "vectors": { + "label": "Vectors", + "key": "vectors", + "type": "codehinter", + "description": "Enter vectors as JSON array", + "placeholder": "[{\"id\": \"vec1\", \"values\": [0.1, 0.2, 0.3]}]", + "height": "36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + } + }, + "update_vector": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "id": { + "label": "ID", + "key": "id", + "type": "codehinter", + "description": "Enter vector ID to update", + "placeholder": "id-3", + "height": "36px" + }, + "values": { + "label": "Values", + "key": "values", + "type": "codehinter", + "description": "Enter updated vector values as JSON array", + "placeholder": "[4.0, 2.0]", + "height": "36px" + }, + "sparse_vector": { + "label": "Sparse Vector", + "key": "sparse_vector", + "type": "codehinter", + "description": "Enter sparse vector values", + "placeholder": "{\"indices\": [1, 5], \"values\": [0.5, 0.5]}", + "height": "36px" + }, + "metadata": { + "label": "Metadata", + "key": "metadata", + "type": "codehinter", + "description": "Enter metadata", + "placeholder": "{\"genre\": \"comedy\"}", + "height": "36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + } + }, + "delete_vectors": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "ids": { + "label": "ID", + "key": "id", + "type": "codehinter", + "description": "Enter vector IDs as JSON array", + "placeholder": "[\"id-1\", \"id-2\"]", + "height": "36px" + }, + "delete_all": { + "label": "Delete All", + "key": "delete_all", + "type": "codehinter", + "description": "Set true to delete all vectors", + "placeholder": "true (false by default)", + "height":"36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + }, + "filter": { + "label": "Filter", + "key": "filter", + "type": "codehinter", + "description": "Enter a filter query in JSON format", + "placeholder": "{\"genre\": {\"$in\": [\"documentary\", \"action\"]}}", + "height": "150px" + } + }, + "query_vector": { + "index": { + "label": "Index", + "key": "index", + "type": "codehinter", + "description": "Enter the index name (e.g., example-index)", + "placeholder": "example-index", + "height": "36px" + }, + "namespace": { + "label": "Namespace", + "key": "namespace", + "type": "codehinter", + "description": "Enter the namespace (optional)", + "placeholder": "example-namespace", + "height": "36px" + }, + "top_k": { + "label": "Top K", + "key": "top_k", + "type": "codehinter", + "description": "Enter the number", + "placeholder": "3", + "height": "36px" + }, + "filter": { + "label": "Filter", + "key": "filter", + "type": "codehinter", + "description": "Enter a filter query in JSON format", + "placeholder": "{\"genre\": {\"$in\": [\"documentary\", \"action\"]}}", + "height": "150px" + }, + "include_values": { + "label": "Include values", + "key": "include_values", + "type": "codehinter", + "description": "Enter boolean values", + "placeholder": "true (false by default)", + "height": "36px" + }, + "include_metadata": { + "label": "Include metadata", + "key": "include_metadata", + "type": "codehinter", + "description": "Enter boolean values", + "placeholder": "true (false by default)", + "height": "36px" + }, + "vectors": { + "label": "Vector", + "key": "vectors", + "type": "codehinter", + "description": "Enter vector IDs as JSON array", + "placeholder": "[\"0.3\", \"0.3\", \"0.3\", \"0.3\", \"0.3\"]", + "height": "36px" + }, + "sparse_vector": { + "label": "Sparse Vector", + "key": "sparse_vector", + "type": "codehinter", + "description": "Enter sparse vector values", + "placeholder": "{\"indices\": [1, 5], \"values\": [0.5, 0.5]}", + "height": "36px" + } + } + } +} diff --git a/marketplace/plugins/pinecone/lib/query_operations.ts b/marketplace/plugins/pinecone/lib/query_operations.ts new file mode 100644 index 0000000000..1e12733f21 --- /dev/null +++ b/marketplace/plugins/pinecone/lib/query_operations.ts @@ -0,0 +1,181 @@ +import { QueryOptions } from './types'; +import { Pinecone } from '@pinecone-database/pinecone'; + +export async function getIndexStats(pinecone: Pinecone, options: QueryOptions): Promise { + const { index } = options; + + if (!index) { + throw new Error('Index name is required'); + } + + try { + const indexClient = pinecone.index(index); + const stats = await indexClient.describeIndexStats(); + + return stats; + } catch (error) { + console.error('Error fetching index stats:', error); + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function listVectorIds(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, prefix, limit, paginationToken, namespace } = options; + + if (!index) { + throw new Error('Index name is required'); + } + + try { + const indexClient = pinecone.index(index); + + const listOptions = { + prefix: prefix, + limit: limit || 10, + paginationToken: paginationToken, + }; + + const client = namespace ? indexClient.namespace(namespace) : indexClient; + + const vectors = await client.listPaginated(listOptions); + + return vectors; + } catch (error) { + console.error('Error listing vector IDs:', error); + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function fetchVectors(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, ids, namespace } = options; + + if (!index || !ids) { + throw new Error('Index name and vector IDs are required'); + } + + const vectorIds = typeof ids === 'string' ? JSON.parse(ids) : ids; + + try { + const indexClient = pinecone.index(index); + const client = namespace ? await indexClient.namespace(namespace) : indexClient; + const vectors = await client.fetch(vectorIds); + + return vectors; + } catch (error) { + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function upsertVectors(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, vectors, namespace } = options; + const parsedVectors = typeof vectors === 'string' ? JSON.parse(vectors) : vectors; + + if (!index || !vectors) { + throw new Error('Index name and vectors are required'); + } + + parsedVectors.forEach((vector) => { + if (!vector.id || !Array.isArray(vector.values)) { + throw new Error('Each vector must have an id and a values array'); + } + }); + + try { + const indexClient = pinecone.index(index); + const client = namespace ? await indexClient.namespace(namespace) : indexClient; + const upsertResponse = await client.upsert(parsedVectors); + if (upsertResponse === undefined) { + return 'Upsert Successful'; + } else { + throw new Error('Upsert failed'); + } + } catch (error) { + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function updateVector(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, id, values, sparse_vector, metadata, namespace } = options; + + if (!index || !id || (!values && !sparse_vector)) { + throw new Error('Index name, vector ID, and either values or sparse vector are required'); + } + + const valuesArray = typeof values === 'string' ? JSON.parse(values) : values; + + try { + const indexClient = pinecone.index(index); + const client = namespace ? await indexClient.namespace(namespace) : indexClient; + const updateResponse = await client.update({ + id, + ...(valuesArray && { values: valuesArray }), + ...(metadata && { metadata: JSON.parse(metadata) }), + ...(sparse_vector && { sparseValues: JSON.parse(sparse_vector) }), + }); + + if (updateResponse === undefined) { + return 'Update Successful'; + } else { + throw new Error('Update failed'); + } + } catch (error) { + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function deleteVectors(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, id, delete_all, namespace, filter } = options; + + if (!index) { + throw new Error('Index name is required'); + } + + try { + const indexClient = pinecone.index(index); + const client = namespace ? await indexClient.namespace(namespace) : indexClient; + let deleteResponse; + if (delete_all && delete_all.toLowerCase() === 'true') { + deleteResponse = await client.deleteAll(); + } else if (filter) { + deleteResponse = await client.deleteMany({ + filter: JSON.parse(filter), + }); + } else { + deleteResponse = await client.deleteMany(JSON.parse(id)); + } + + if (deleteResponse === undefined) { + return 'Delete Successful'; + } else { + throw new Error('Delete failed'); + } + } catch (error) { + throw new Error(error?.message || 'An unexpected error occurred'); + } +} + +export async function quertVectors(pinecone: Pinecone, options: QueryOptions): Promise { + const { index, namespace, top_k, filter, include_values, include_metadata, vectors, sparse_vector } = options; + + if (!index) { + throw new Error('Index is required'); + } + + const pineconeQueryOptions = { + topK: Number(top_k), + vector: JSON.parse(vectors), + ...(filter && { filter: JSON.parse(filter) }), + ...(include_values && { includeValues: include_values.toLowerCase() === 'true' }), + ...(include_metadata && { includeMetadata: include_metadata.toLowerCase() === 'true' }), + ...(sparse_vector && { sparseVector: JSON.parse(sparse_vector) }), + }; + + try { + const indexClient = pinecone.index(index); + const client = namespace ? await indexClient.namespace(namespace) : indexClient; + const queryResponse = await client.query(pineconeQueryOptions); + return queryResponse; + } catch (error) { + throw new Error(error.message); + } +} diff --git a/marketplace/plugins/pinecone/lib/types.ts b/marketplace/plugins/pinecone/lib/types.ts new file mode 100644 index 0000000000..44a670264a --- /dev/null +++ b/marketplace/plugins/pinecone/lib/types.ts @@ -0,0 +1,44 @@ +export type SourceOptions = { + apiKey: string; +}; + +// Define the query options based on the available operations in the operations.json file. +export type QueryOptions = { + operation: Operation; + index: string; + ids?: string[]; + vectors?: string; + string?: string[]; + id?: string; + values?: number[]; + sparseValues?: SparseValues; + setmetadata?: object; + filter?: string; + prefix?: string; + limit?: number; + paginationToken?: string; + namespace?: string; + delete_all?: string; + metadata?: string; + sparse_vector?: string; + top_k?: string; + include_metadata?: string; + include_values?: string; +}; + +// Define a type for sparse vectors used in the "update_vector" operation. +export type SparseValues = { + indices: number[]; + values: number[]; +}; + +// Enum for different operations. +export enum Operation { + GetIndexStats = 'get_index_stats', + ListVectorIds = 'list_vector_ids', + FetchVectors = 'fetch_vectors', + UpsertVectors = 'upsert_vectors', + UpdateVector = 'update_vector', + DeleteVectors = 'delete_vectors', + QueryVectors = 'query_vector', +} diff --git a/marketplace/plugins/pinecone/package.json b/marketplace/plugins/pinecone/package.json new file mode 100644 index 0000000000..aa65c9a95a --- /dev/null +++ b/marketplace/plugins/pinecone/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tooljet-marketplace/pinecone", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1", + "build": "ncc build lib/index.ts -o dist", + "watch": "ncc build lib/index.ts -o dist --watch" + }, + "homepage": "https://github.com/tooljet/tooljet#readme", + "dependencies": { + "@grpc/grpc-js": "^1.12.2", + "@pinecone-database/pinecone": "^3.0.3", + "@tooljet-marketplace/common": "^1.0.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.34.0", + "typescript": "^4.7.4" + } +} diff --git a/marketplace/plugins/pinecone/tsconfig.json b/marketplace/plugins/pinecone/tsconfig.json new file mode 100644 index 0000000000..a18a801b14 --- /dev/null +++ b/marketplace/plugins/pinecone/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "lib" + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/server/src/assets/marketplace/plugins.json b/server/src/assets/marketplace/plugins.json index c2e021b0be..7f8c2f2742 100644 --- a/server/src/assets/marketplace/plugins.json +++ b/server/src/assets/marketplace/plugins.json @@ -111,5 +111,21 @@ "id": "jira", "author": "Tooljet", "timestamp": "Thu, 08 Aug 2024 11:04:36 GMT" + }, + { + "name": "Portkey", + "description": "Plugin for Portkey APIs", + "version": "1.0.0", + "id": "portkey", + "author": "Portkey", + "timestamp": "Sat, 29 Jun 2024 09:40:13 GMT" + }, + { + "name": "Pinecone", + "description": "Plugin for Pinecone Vector DB", + "version": "1.0.0", + "id": "pinecone", + "author": "Tooljet", + "timestamp": "Mon, 28 Oct 2024 08:08:28 GMT" } -] \ No newline at end of file +] From dd2f6438f4c6e56b865020c062d0ccc69ca7c38b Mon Sep 17 00:00:00 2001 From: Parth <108089718+parthy007@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:59:23 +0530 Subject: [PATCH 017/104] Update sort changes correctly (#2590) --- frontend/src/_ui/Sort/index.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/_ui/Sort/index.js b/frontend/src/_ui/Sort/index.js index 2edf03c643..323d9d26e5 100644 --- a/frontend/src/_ui/Sort/index.js +++ b/frontend/src/_ui/Sort/index.js @@ -26,14 +26,9 @@ export default ({ } function keyValuePairValueChanged(value, keyIndex, index) { - if (!isRenderedAsQueryEditor) { - const newOptions = deepClone(options); - newOptions[index][keyIndex] = value; - options.length - 1 === index ? addNewKeyValuePair(newOptions) : optionchanged(getter, newOptions); - } else { - options[index][keyIndex] = value; - optionchanged(getter, options); - } + const newOptions = deepClone(options); + newOptions[index][keyIndex] = value; + options.length - 1 === index ? addNewKeyValuePair(newOptions) : optionchanged(getter, newOptions); } const commonProps = { From 9f499354660b574ea5f7dcd8a47c6530632dcfe7 Mon Sep 17 00:00:00 2001 From: Anantshree Chandola Date: Thu, 7 Nov 2024 11:54:54 +0530 Subject: [PATCH 018/104] resolve params in runJS (#2574) --- server/src/services/data_queries.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/services/data_queries.service.ts b/server/src/services/data_queries.service.ts index 16d5113279..81509bc95d 100644 --- a/server/src/services/data_queries.service.ts +++ b/server/src/services/data_queries.service.ts @@ -649,7 +649,16 @@ export class DataQueriesService { const variables = resolvedValue.match(/\{\{(.*?)\}\}/g); for (const variable of variables || []) { - resolvedValue = resolvedValue.replace(variable, options[variable]); + let replacement = options[variable]; + // Check if the replacement is an object + if (typeof replacement === 'object' && replacement !== null) { + // Ensure parent is a non-empty array before attempting to access its first element + if (Array.isArray(parent) && parent.length > 0) { + // Assign replacement value based on the first item in the parent array + replacement = replacement[parent[0]]; + } + } + resolvedValue = resolvedValue.replace(variable, replacement); } if (parent && key !== null) { parent[key] = resolvedValue; From af9ed6fcfa169c535ecb921f2333e19b8e2b40a8 Mon Sep 17 00:00:00 2001 From: Manish Kushare <37823141+manishkushare@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:26:07 +0530 Subject: [PATCH 019/104] Bug fixed (#2587) --- .../QueryEditors/TooljetDatabase/JoinConstraint.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx index 0a7f9ab6dc..922608fb74 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx @@ -371,7 +371,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => { variant="ghostBlue" size="sm" onClick={() => { - const newData = { ...data }; + const newData = deepClone(data); set(newData, 'conditions.conditionsList', [...conditionsList, { operator: '=' }]); onChange(newData); }} From 270bb703ae44d1788cc302b0dbfdbfefcb379aa8 Mon Sep 17 00:00:00 2001 From: Qasim Ahmad <55128282+qasimahmadkhan@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:29:26 +0500 Subject: [PATCH 020/104] Remove Last Edited User Info (#10712) --- frontend/src/HomePage/AppCard.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/HomePage/AppCard.jsx b/frontend/src/HomePage/AppCard.jsx index d8cd4b3674..28c4e3dc83 100644 --- a/frontend/src/HomePage/AppCard.jsx +++ b/frontend/src/HomePage/AppCard.jsx @@ -114,8 +114,6 @@ export default function AppCard({ {updated === 'just now' ? `Edited ${updated}` : `Edited ${updated} ago`} -  by{' '} - {`${app.user?.first_name ? app.user.first_name : ''} ${app.user?.last_name ? app.user.last_name : ''}`}
)}
From b51911fec5a6c61027ae5654fcfa86f68fd7b81e Mon Sep 17 00:00:00 2001 From: Rudhra Deep Biswas <98055396+rudeUltra@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:16:06 +0530 Subject: [PATCH 021/104] migration fixes (#11241) --- .../1720352990850-CreateDefaultGroupInExistingWorkspace.ts | 4 +++- ...65772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/server/data-migrations/1720352990850-CreateDefaultGroupInExistingWorkspace.ts b/server/data-migrations/1720352990850-CreateDefaultGroupInExistingWorkspace.ts index 40135eb3db..c2886e7066 100644 --- a/server/data-migrations/1720352990850-CreateDefaultGroupInExistingWorkspace.ts +++ b/server/data-migrations/1720352990850-CreateDefaultGroupInExistingWorkspace.ts @@ -106,7 +106,9 @@ export class CreateDefaultGroupInExistingWorkspace1720352990850 implements Migra admin: 'admin', }) .getMany(); - const userIds = adminsUsers.map((userGroup) => userGroup.userId); + const uniqueUserIds = new Set(adminsUsers.map((userGroup) => userGroup.userId)); + if (uniqueUserIds.size === 0) continue; + const userIds = [...uniqueUserIds]; await this.migrateUserGroup(manager, userIds, group.id); } } diff --git a/server/data-migrations/1720365772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts b/server/data-migrations/1720365772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts index 049681146e..46699c86fb 100644 --- a/server/data-migrations/1720365772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts +++ b/server/data-migrations/1720365772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts @@ -69,7 +69,9 @@ export class AddingUsersToRespectiveRolesBuilderAndEndUsers1720365772516 impleme organizationId, } ) - .innerJoin('users.apps', 'apps') + .innerJoin('users.apps', 'apps', 'apps.organizationId = :organizationId', { + organizationId, + }) .select('users.id') .distinct() .getMany() From 67c07fb4c9b315ec07d9d610305be63801a77538 Mon Sep 17 00:00:00 2001 From: Parth <108089718+parthy007@users.noreply.github.com> Date: Sat, 9 Nov 2024 14:59:52 +0530 Subject: [PATCH 022/104] Fix sort-inputs delete logic (#2608) --- frontend/src/_ui/Sort/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/_ui/Sort/index.js b/frontend/src/_ui/Sort/index.js index 323d9d26e5..21a5fa7a91 100644 --- a/frontend/src/_ui/Sort/index.js +++ b/frontend/src/_ui/Sort/index.js @@ -21,8 +21,9 @@ export default ({ } function removeKeyValuePair(index) { - options.splice(index, 1); - optionchanged(getter, options); + const newOptions = [...options]; + newOptions.splice(index, 1); + optionchanged(getter, newOptions); } function keyValuePairValueChanged(value, keyIndex, index) { From 3f87ebd9cc4355d3cc4187d1d87c07deea264d39 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:59:58 +0530 Subject: [PATCH 023/104] Feat: User with builder role can access ToolJet database (#2609) * feat: users with builder role can access tooljet database * fix: review comments --- frontend/src/TooljetDatabase/index.jsx | 4 +++- frontend/src/_components/LogoNavDropdown.jsx | 15 +++++++++++++-- frontend/src/_helpers/utils.js | 11 +++++++++++ frontend/src/_ui/Layout/index.jsx | 5 ++++- .../casl/abilities/tooljet-db-ability.factory.ts | 4 +++- .../src/services/permissions-ability.service.ts | 11 +++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/frontend/src/TooljetDatabase/index.jsx b/frontend/src/TooljetDatabase/index.jsx index 19d1b53109..72c179949d 100644 --- a/frontend/src/TooljetDatabase/index.jsx +++ b/frontend/src/TooljetDatabase/index.jsx @@ -6,6 +6,7 @@ import { authenticationService } from '../_services/authentication.service'; import { BreadCrumbContext } from '@/App/App'; import { useNavigate } from 'react-router-dom'; import { pageTitles, fetchAndSetWindowTitle } from '@white-label/whiteLabelling'; +import { hasBuilderRole } from '@/_helpers/utils'; export const TooljetDatabaseContext = createContext({ organizationId: null, @@ -72,8 +73,9 @@ export const TooljetDatabase = (props) => { }; const navigate = useNavigate(); const { admin } = authenticationService.currentSessionValue; + const isBuilder = hasBuilderRole(authenticationService?.currentSessionValue?.role ?? {}); - if (!admin) { + if (!admin && !isBuilder) { navigate('/'); } diff --git a/frontend/src/_components/LogoNavDropdown.jsx b/frontend/src/_components/LogoNavDropdown.jsx index 76ce395087..db45ffd05e 100644 --- a/frontend/src/_components/LogoNavDropdown.jsx +++ b/frontend/src/_components/LogoNavDropdown.jsx @@ -4,7 +4,8 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import { authenticationService } from '@/_services'; import { getPrivateRoute, redirectToDashboard, dashboardUrl } from '@/_helpers/routes'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import AppLogo from './AppLogo'; +import AppLogo from '@/_components/AppLogo'; +import { hasBuilderRole } from '@/_helpers/utils'; export default function LogoNavDropdown({ darkMode }) { const handleBackClick = (e) => { @@ -15,6 +16,16 @@ export default function LogoNavDropdown({ darkMode }) { const getOverlay = () => { const { admin } = authenticationService?.currentSessionValue ?? {}; + const data_source_group_permissions = authenticationService?.currentSessionValue?.data_source_group_permissions; + const showDataSource = + authenticationService?.currentSessionValue?.user_permissions?.data_source_create === true || + authenticationService?.currentSessionValue?.user_permissions?.data_source_delete === true || + data_source_group_permissions?.usable_data_sources_id?.length || + data_source_group_permissions?.is_all_usable || + data_source_group_permissions?.configurable_data_source_id?.length || + data_source_group_permissions?.is_all_configurable; + const isBuilder = hasBuilderRole(authenticationService?.currentSessionValue?.role ?? {}); + return (
Back to apps
- {admin && ( + {(admin || isBuilder) && ( { return 'Password can be at max 100 characters long'; } }; + +export const checkConditionsForRoute = (conditions, conditionsObj) => { + if (!conditions || conditions.length === 0) { + return true; + } + return conditions.every((condition) => conditionsObj?.[condition] === true); +}; + +export const hasBuilderRole = (roleObj) => { + return roleObj.name === 'builder'; +}; diff --git a/frontend/src/_ui/Layout/index.jsx b/frontend/src/_ui/Layout/index.jsx index 43d8afc239..e9ed74aa22 100644 --- a/frontend/src/_ui/Layout/index.jsx +++ b/frontend/src/_ui/Layout/index.jsx @@ -13,6 +13,8 @@ import { ConfirmDialog } from '@/_components'; import useGlobalDatasourceUnsavedChanges from '@/_hooks/useGlobalDatasourceUnsavedChanges'; import Settings from '@/_components/Settings'; import { retrieveWhiteLabelLogo, fetchWhiteLabelDetails } from '@white-label/whiteLabelling'; +import '../../_styles/left-sidebar.scss'; +import { hasBuilderRole } from '@/_helpers/utils'; function Layout({ children, @@ -27,6 +29,7 @@ function Layout({ const admin = currentUserValue?.admin; fetchWhiteLabelDetails(); const whiteLabelLogo = retrieveWhiteLabelLogo(); + const isBuilder = hasBuilderRole(authenticationService?.currentSessionValue?.role ?? {}); const { checkForUnsavedChanges, @@ -86,7 +89,7 @@ function Layout({ - {admin && ( + {(admin || isBuilder) && (
  • { + return USER_ROLE.BUILDER === (await this.getUserRole(user.id, user.organizationId)); + } + + async getUserRole(userId: string, organizationId: string): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + return (await getUserRoleQuery(userId, organizationId, manager).getOne()).name; + }); + } } From 5c628ba2296e5da16866248bba1ab7f7283651a2 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Sat, 9 Nov 2024 19:04:24 +0530 Subject: [PATCH 024/104] fix: renamed REST api datasource (#2549) --- .../src/Editor/DataSourceManager/SourceComponents/index.js | 4 ++-- plugins/packages/restapi/lib/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Editor/DataSourceManager/SourceComponents/index.js b/frontend/src/Editor/DataSourceManager/SourceComponents/index.js index b069421c69..92eff8655e 100644 --- a/frontend/src/Editor/DataSourceManager/SourceComponents/index.js +++ b/frontend/src/Editor/DataSourceManager/SourceComponents/index.js @@ -13,7 +13,7 @@ export const CommonlyUsedDataSources = Object.keys(allManifests) .reduce((accumulator, currentValue) => { const sourceName = allManifests[currentValue]?.source?.name; if ( - sourceName === 'Rest API' || + sourceName === 'REST API' || sourceName === 'MongoDB' || sourceName === 'Airtable' || sourceName === 'Google Sheets' || @@ -27,7 +27,7 @@ export const CommonlyUsedDataSources = Object.keys(allManifests) return accumulator; }, []) .sort((a, b) => { - const order = ['Rest API', 'PostgreSQL', 'Google Sheets', 'Airtable', 'MongoDB']; + const order = ['REST API', 'PostgreSQL', 'Google Sheets', 'Airtable', 'MongoDB']; return order.indexOf(a.name) - order.indexOf(b.name); }); diff --git a/plugins/packages/restapi/lib/manifest.json b/plugins/packages/restapi/lib/manifest.json index 4fa195d430..646749e98d 100644 --- a/plugins/packages/restapi/lib/manifest.json +++ b/plugins/packages/restapi/lib/manifest.json @@ -4,7 +4,7 @@ "description": "A schema defining restapi datasource", "type": "api", "source": { - "name": "Rest API", + "name": "REST API", "kind": "restapi", "options": { "url": { From 04c032f5a21b694d0901c230f89c8552cbfff9cd Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Sat, 9 Nov 2024 21:11:24 +0530 Subject: [PATCH 025/104] add retry toggle on restapi datasource config --- plugins/packages/restapi/lib/manifest.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/packages/restapi/lib/manifest.json b/plugins/packages/restapi/lib/manifest.json index 646749e98d..530facf965 100644 --- a/plugins/packages/restapi/lib/manifest.json +++ b/plugins/packages/restapi/lib/manifest.json @@ -166,7 +166,7 @@ }, "properties": { "renderForm":{ - "type":"react-form-component", + "type": "react-form-component", "credentialsInputs": { "title": "CREDENTIALS", "inputs": { @@ -208,7 +208,7 @@ }, "authenticationInputs": { "title": "AUTHENTICATION", - "inputs":{ + "inputs": { "auth_type": { "label": "Authentication type", "key": "auth_type", @@ -219,7 +219,7 @@ }, "sslInputs": { "title": "SECURE SOCKETS LAYER", - "inputs":{ + "inputs": { "ssl_certificate": { "label": "SSL Certificate", "key": "ssl_certificate", @@ -264,6 +264,18 @@ } } } + }, + "generalSettingInputs": { + "title": "GENERAL SETTINGS", + "inputs": { + "retry_network_errors": { + "text": "Retry on network errors", + "subtext": "By default, ToolJet tries to hit API endpoint 3 times before declaring query failed as server did not respond", + "key": "retry_network_errors", + "type": "toggle", + "description": "key-value pair headers for rest api" + } + } } } }, From b4c16e898cd3a5e43cc353a8dab350b9338e3b61 Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Sat, 9 Nov 2024 21:41:01 +0530 Subject: [PATCH 026/104] bump to v3.0.0-ce-beta.1 --- .version | 2 +- frontend/.version | 2 +- server/.version | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.version b/.version index 8916c6188b..b56e89db41 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.0.0-ce-beta \ No newline at end of file +3.0.0-ce-beta.1 diff --git a/frontend/.version b/frontend/.version index 8916c6188b..b56e89db41 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.0.0-ce-beta \ No newline at end of file +3.0.0-ce-beta.1 diff --git a/server/.version b/server/.version index 8916c6188b..b56e89db41 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -3.0.0-ce-beta \ No newline at end of file +3.0.0-ce-beta.1 From 67b3ea2edbd54664e306d480ba94a82ef2d9b192 Mon Sep 17 00:00:00 2001 From: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:09:35 +0530 Subject: [PATCH 027/104] Array property added to dependency graph --- .../_stores/slices/componentsSlice.js | 224 +++++++++++++++--- frontend/src/AppBuilder/_stores/utils.js | 31 +++ 2 files changed, 216 insertions(+), 39 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index d5b0afa9d4..e5b10d2120 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -4,12 +4,12 @@ import { resolveDynamicValues, // extractAndReplaceReferencesFromString, checkSubstringRegex, + hasArrayNotation, + parsePropertyPath, } from '@/AppBuilder/_stores/utils'; import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast'; -import { componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; -import { v4 as uuidv4 } from 'uuid'; -import _, { cloneDeep, merge } from 'lodash'; +import { cloneDeep, merge, set as lodashSet } from 'lodash'; import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils'; import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig'; import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants'; @@ -243,7 +243,8 @@ export const createComponentsSlice = (set, get) => ({ property, value, component, - componentResolvedValues = {}, // If componentResolvedValues is null, then it is from a setComponentProperty call + componentResolvedValues = {}, + updatePassedValue = true, moduleId ) => { const { @@ -281,7 +282,7 @@ export const createComponentsSlice = (set, get) => ({ allRefs.push(customResolvablePath); } } - if (componentResolvedValues !== null) + if (updatePassedValue) setAllValueToComponent( componentDetails, valueWithBrackets, @@ -294,7 +295,7 @@ export const createComponentsSlice = (set, get) => ({ return { updatedValue: valueWithId, allRefs, unResolvedValue: valueWithBrackets, componentResolvedValues }; } else { - if (componentResolvedValues !== null) + if (updatePassedValue) setAllValueToComponent( componentDetails, value, @@ -317,7 +318,7 @@ export const createComponentsSlice = (set, get) => ({ componentResolvedValues = {}, moduleId ) => { - const { getAllExposedValues } = get(); + const { getAllExposedValues, getComponentTypeFromId } = get(); const { componentId, paramType, property } = componentDetails; const length = Object.keys(customResolvables).length; if (length === 0) { @@ -334,12 +335,31 @@ export const createComponentsSlice = (set, get) => ({ if (!componentResolvedValues[componentId][index][paramType]) { componentResolvedValues[componentId][index][paramType] = {}; } - componentResolvedValues[componentId][index][paramType][property] = resolvedValue; + if (hasArrayNotation(property)) { + const keys = parsePropertyPath(property); + lodashSet( + componentResolvedValues, + [componentId, index, paramType, ...keys], + getComponentTypeFromId(componentId) === 'Table' ? value : resolvedValue + ); + } else { + componentResolvedValues[componentId][index][paramType][property] = resolvedValue; + } } else { if (!componentResolvedValues[componentId][paramType]) { componentResolvedValues[componentId][paramType] = {}; } - componentResolvedValues[componentId][paramType][property] = resolvedValue; + + if (hasArrayNotation(property)) { + const keys = parsePropertyPath(property); + lodashSet( + componentResolvedValues, + [componentId, paramType, ...keys], + getComponentTypeFromId(componentId) === 'Table' ? value : resolvedValue + ); + } else { + componentResolvedValues[componentId][paramType][property] = resolvedValue; + } } } else { // Loop all the index and set the resolved value @@ -564,6 +584,63 @@ export const createComponentsSlice = (set, get) => ({ }; }, + // This function checks whether the property value is an array or not and then resolves the value accordingly + // Cases like Table column, Dropdown options, etc. + checkValueAndResolve: ( + componentId, + paramType, + property, + value, + component, + resolvedComponentValues, + updatePassedValue = true, + moduleId + ) => { + const { updateResolvedValues, generateDependencyGraphForRefs } = get(); + const updatedPropertyValue = cloneDeep(value); + if (Array.isArray(value)) { + value.forEach((val, index) => { + Object.entries(val).forEach(([key, keyValue]) => { + const propertyWithArrayValue = `${property}[${index}].${key}`; + const keys = [key]; + if (keyValue?.value) { + keys.push('value'); + } + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + propertyWithArrayValue, + keyValue?.value ?? keyValue, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + lodashSet(updatedPropertyValue, [index, ...keys], updatedValue); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + } + }); + }); + return { updatedValue: updatedPropertyValue }; + } else { + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + property, + value, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue); + } + return { allRefs, unResolvedValue, updatedValue }; + } + }, + updateDependencyGraphAndResolvedValues: ( moduleId, componentId, @@ -572,22 +649,20 @@ export const createComponentsSlice = (set, get) => ({ resolvedComponentValues = {}, paramType ) => { - const { updateResolvedValues, generateDependencyGraphForRefs, setAllValueToComponent } = get(); + const { checkValueAndResolve, setAllValueToComponent } = get(); if (component.definition[paramType] === undefined) return; Object.entries(component.definition[paramType]).forEach(([property, value]) => { if (!value?.skipResolve) { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value?.value, component, resolvedComponentValues, + true, moduleId ); - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue); - } } else { const componentDetails = { componentId, paramType, property }; setAllValueToComponent(componentDetails, value?.value, false, null, {}, resolvedComponentValues, moduleId); @@ -925,11 +1000,10 @@ export const createComponentsSlice = (set, get) => ({ saveComponentChanges, withUndoRedo, getComponentTypeFromId, - updateResolvedValues, - generateDependencyGraphForRefs, setResolvedComponent, getComponentDefinition, currentLayout, + checkValueAndResolve, } = get(); let hasParentChanged = false; let oldParentId; @@ -1003,19 +1077,16 @@ export const createComponentsSlice = (set, get) => ({ objectsToUpdate.forEach((paramType) => { if (component.definition[paramType]) { Object.entries(component.definition[paramType]).forEach(([property, value]) => { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value.value, component, resolvedComponentValues, + true, moduleId ); - - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true); - } }); } }); @@ -1066,22 +1137,65 @@ export const createComponentsSlice = (set, get) => ({ removeDependency, getComponentDefinition, setValueToComponent, + checkValueAndResolve, + getResolvedComponent, + setResolvedComponent, } = get(); const { component } = getComponentDefinition(componentId, moduleId); + const oldValue = component.definition[paramType][property]; + + if (Array.isArray(oldValue?.value)) { + const resolvedComponent = { [componentId]: deepClone(getResolvedComponent(componentId) ?? {}) }; + const { updatedValue } = checkValueAndResolve( + componentId, + paramType, + property, + value, + component, + resolvedComponent, + true, + moduleId + ); + setResolvedComponent(componentId, resolvedComponent[componentId], moduleId); + + // If the value is not changed, return + if (oldValue?.[attr] === updatedValue || oldValue === updatedValue) return; + + set( + withUndoRedo((state) => { + const pageComponent = state.modules[moduleId].pages[currentPageIndex].components[componentId].component; + lodashSet(pageComponent, ['definition', paramType, property, attr], updatedValue); + }, skipUndoRedo), + false, + 'setComponentProperty' + ); + + const diff = { + [componentId]: { component: get().modules[moduleId].pages[currentPageIndex].components[componentId].component }, + }; + + if (saveAfterAction) { + const currentMode = get().currentMode; + if (currentMode !== 'view') saveComponentChanges(diff, 'components', 'update'); + + get().multiplayer.broadcastUpdates({ componentId, property, value, paramType, attr }, 'components', 'update'); + } + return; + } + // Update the value and get new dependencies const { updatedValue, allRefs, unResolvedValue } = attr === 'value' && !skipResolve - ? updateResolvedValues(componentId, paramType, property, value, component, null, moduleId) + ? updateResolvedValues(componentId, paramType, property, value, component, null, false, moduleId) : { updatedValue: value, allRefs: [], unResolvedValue: value }; // If the value is not changed, return - const oldValue = component.definition[paramType][property]; if (oldValue?.[attr] === updatedValue || oldValue === updatedValue) return; set( withUndoRedo((state) => { const pageComponent = state.modules[moduleId].pages[currentPageIndex].components[componentId].component; - _.set(pageComponent, ['definition', paramType, property, attr], updatedValue); + lodashSet(pageComponent, ['definition', paramType, property, attr], updatedValue); }, skipUndoRedo), false, 'setComponentProperty' @@ -1130,9 +1244,8 @@ export const createComponentsSlice = (set, get) => ({ const { currentPageIndex, saveComponentChanges, - updateResolvedValues, + checkValueAndResolve, getComponentDefinition, - generateDependencyGraphForRefs, getComponentTypeFromId, setResolvedComponent, withUndoRedo, @@ -1189,19 +1302,16 @@ export const createComponentsSlice = (set, get) => ({ objectsToUpdate.forEach((paramType) => { if (component.definition[paramType]) { Object.entries(component.definition[paramType]).forEach(([property, value]) => { - const { allRefs, unResolvedValue } = updateResolvedValues( + checkValueAndResolve( componentId, paramType, property, value.value, component, resolvedComponentValues, + true, moduleId ); - - if (allRefs.length) { - generateDependencyGraphForRefs(allRefs, componentId, paramType, property, unResolvedValue, true); - } }); } }); @@ -1427,6 +1537,8 @@ export const createComponentsSlice = (set, get) => ({ getNodeData, getEntityResolvedValueLength, updateChildComponentResolvedValues, + getComponentTypeFromId, + getResolvedComponent, } = get(); const dependecies = getDependencies(path, moduleId); if (dependecies?.length) { @@ -1436,7 +1548,8 @@ export const createComponentsSlice = (set, get) => ({ if (itemsLength) { updateChildComponentResolvedValues(dependency, path, itemsLength, moduleId); } else { - const [entityType, entityId, type, key] = dependency.split('.'); + const [entityType, entityId, type, ...keys] = dependency.split('.'); + const key = keys.join('.'); const unResolvedValue = getNodeData(dependency); const resolvedValue = resolveDynamicValues(unResolvedValue, getAllExposedValues(), {}, false, []); @@ -1455,13 +1568,46 @@ export const createComponentsSlice = (set, get) => ({ ? get().debugger.validateProperty(entityId, type, key, resolvedValue) : resolvedValue; - set( - (state) => { - state.resolvedStore.modules[moduleId][entityType][entityId][type][key] = validatedValue; - }, - false, - 'updateDependencyValues' - ); + // logic to handle the key like options[0].visible. It will resolve the visible directly and update the resolved store + if (hasArrayNotation(key)) { + const keys = parsePropertyPath(key); + // Triggering a re-render of the table component if any of the dependent component is updated + // This is done to calculate the callValues in the table component + // Need to find a better way to handle this + if (getComponentTypeFromId(entityId, moduleId) === 'Table') { + set( + (state) => { + lodashSet( + state.resolvedStore.modules[moduleId][entityType][entityId], + ['properties', 'shouldRender'], + (getResolvedComponent(entityId)?.['properties']?.['shouldRender'] ?? 0) + 1 + ); + }, + false, + 'updateDependencyValues' + ); + } else { + set( + (state) => { + lodashSet( + state.resolvedStore.modules[moduleId][entityType][entityId], + [type, ...keys], + getComponentTypeFromId(entityId, moduleId) === 'Table' ? unResolvedValue + ' ' : validatedValue + ); + }, + false, + 'updateDependencyValues' + ); + } + } else { + set( + (state) => { + state.resolvedStore.modules[moduleId][entityType][entityId][type][key] = validatedValue; + }, + false, + 'updateDependencyValues' + ); + } } } }); diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js index 20570b9b36..cc598af3a9 100644 --- a/frontend/src/AppBuilder/_stores/utils.js +++ b/frontend/src/AppBuilder/_stores/utils.js @@ -672,3 +672,34 @@ export function convertAllKeysToSnakeCase(o) { // return { suggestionList, hintsMap, resolvedRefs }; // } + +export const hasArrayNotation = (property) => { + // Regular expression to match array notation pattern + const arrayPattern = /\[\d+\]/; + return arrayPattern.test(property); +}; + +export const parsePropertyPath = (property) => { + // Split the property path into segments + const segments = property.split('.'); + const result = []; + + for (const segment of segments) { + // Check if segment contains array notation + if (hasArrayNotation(segment)) { + // Extract the property name and array index + const [name, ...rest] = segment.split('['); + if (name) result.push(name); + + // Extract and clean up array indices + for (const item of rest) { + const index = parseInt(item.replace(']', '')); + result.push(index); + } + } else { + result.push(segment); + } + } + + return result; +}; From 49349e2d5af6862b27fc6a30a5b8cb4ef0be4b93 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma <79473274+shaurya-sharma064@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:32:11 +0530 Subject: [PATCH 028/104] Fixed multiselect & dropdown issues --- .../Components/QueryManagerBody.jsx | 1 + .../_stores/slices/componentsSlice.js | 42 +++++++++++++++---- .../Components/DropdownV2/DropdownV2.jsx | 20 +++++---- .../MultiselectV2/MultiselectV2.jsx | 39 +++++++++++------ server/src/helpers/components.helper.ts | 8 ++-- server/src/services/components.service.ts | 10 ++--- 6 files changed, 84 insertions(+), 36 deletions(-) diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index c4c820ff69..2a7be4a37d 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -311,6 +311,7 @@ export const QueryManagerBody = ({ darkMode, options, setOptions, activeTab }) = ); }; + // if (selectedQueryId !== selectedQuery?.id) return; const hasPermissions = selectedDataSource?.scope === 'global' && selectedDataSource?.type !== DATA_SOURCE_TYPE.SAMPLE ? canUpdateDataSource(selectedQuery?.data_source_id) || diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index e5b10d2120..63f7e4cb4a 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -600,27 +600,47 @@ export const createComponentsSlice = (set, get) => ({ const updatedPropertyValue = cloneDeep(value); if (Array.isArray(value)) { value.forEach((val, index) => { - Object.entries(val).forEach(([key, keyValue]) => { - const propertyWithArrayValue = `${property}[${index}].${key}`; - const keys = [key]; - if (keyValue?.value) { - keys.push('value'); - } + //This code assumes that the array always consists of objects the else condition is to handle the case when the value is an array of strings/numbers + if (typeof val === 'object') { + Object.entries(val).forEach(([key, keyValue]) => { + const propertyWithArrayValue = `${property}[${index}].${key}`; + const keys = [key]; + if (keyValue?.value) { + keys.push('value'); + } + const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( + componentId, + paramType, + propertyWithArrayValue, + keyValue?.value ?? keyValue, + component, + resolvedComponentValues, + updatePassedValue, + moduleId + ); + lodashSet(updatedPropertyValue, [index, ...keys], updatedValue); + if (allRefs.length) { + generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); + } + }); + } else { + const propertyWithArrayValue = `${property}[${index}]`; const { allRefs, unResolvedValue, updatedValue } = updateResolvedValues( componentId, paramType, propertyWithArrayValue, - keyValue?.value ?? keyValue, + val, component, resolvedComponentValues, updatePassedValue, moduleId ); - lodashSet(updatedPropertyValue, [index, ...keys], updatedValue); + updatedPropertyValue[index] = updatedValue; + console.log('updatedPropertyValue', updatedPropertyValue); if (allRefs.length) { generateDependencyGraphForRefs(allRefs, componentId, paramType, propertyWithArrayValue, unResolvedValue); } - }); + } }); return { updatedValue: updatedPropertyValue }; } else { @@ -1156,6 +1176,10 @@ export const createComponentsSlice = (set, get) => ({ true, moduleId ); + resolvedComponent[componentId][paramType][property] = resolvedComponent[componentId][paramType][property].slice( + 0, + value.length + ); setResolvedComponent(componentId, resolvedComponent[componentId], moduleId); // If the value is not changed, return diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index 1acddf4700..8b554ec3c2 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -90,7 +90,6 @@ export const DropdownV2 = ({ } = styles; const isInitialRender = useRef(true); const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value)); - const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); const isMandatory = validation?.mandatory ?? false; const options = properties?.options; const [validationStatus, setValidationStatus] = useState(validate(currentValue)); @@ -115,12 +114,12 @@ export const DropdownV2 = ({ let _options = advanced ? schema : options; if (Array.isArray(_options)) { let _selectOptions = _options - .filter((data) => getResolvedValue(advanced ? data?.visible : data?.visible?.value) ?? true) + .filter((data) => data?.visible ?? true) .map((data) => ({ ...data, - label: String(getResolvedValue(data?.label)), - value: getResolvedValue(data?.value), - isDisabled: getResolvedValue(advanced ? data?.disable : data?.disable?.value) ?? false, + label: data?.label, + value: data?.value, + isDisabled: data?.disable ?? false, })); return _selectOptions; @@ -177,9 +176,16 @@ export const DropdownV2 = ({ useEffect(() => { if (advanced) { setInputValue(findDefaultItem(schema)); - } else setInputValue(value); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, value, JSON.stringify(schema)]); + }, [advanced, JSON.stringify(schema)]); + + useEffect(() => { + if (!advanced) { + setInputValue(value); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [advanced, value]); useEffect(() => { document.addEventListener('mousedown', handleOutsideClick); diff --git a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx index 960955de57..2efe2d29b5 100644 --- a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx +++ b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx @@ -72,7 +72,6 @@ export const MultiselectV2 = ({ const valueContainerRef = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isMultiSelectLoading, setIsMultiSelectLoading] = useState(multiSelectLoadingState); - const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); const [isMultiSelectDisabled, setIsMultiSelectDisabled] = useState(disabledState); const [isSelectAllSelected, setIsSelectAllSelected] = useState(false); const [searchInputValue, setSearchInputValue] = useState(''); @@ -91,12 +90,12 @@ export const MultiselectV2 = ({ const _options = advanced ? schema : options; let _selectOptions = Array.isArray(_options) ? _options - .filter((data) => getResolvedValue(advanced ? data?.visible : data?.visible?.value) ?? true) + .filter((data) => data?.visible ?? true) .map((data) => ({ ...data, - label: getResolvedValue(data?.label), - value: getResolvedValue(data?.value), - isDisabled: getResolvedValue(advanced ? data?.disable : data?.disable?.value) ?? false, + label: data?.label, + value: data?.value, + isDisabled: data?.disable ?? false, })) : []; return _selectOptions; @@ -129,9 +128,10 @@ export const MultiselectV2 = ({ } const onChangeHandler = (items, action) => { setInputValue(items); - if (action.action === 'select-option') { - fireEvent('onSelect'); - } + fireEvent('onSelect'); + // if (action.action === 'select-option') { + // fireEvent('onSelect'); + // } }; useEffect(() => { @@ -141,10 +141,20 @@ export const MultiselectV2 = ({ }, [selectOptions]); useEffect(() => { - let foundItem = findDefaultItem(values, advanced, true); - setInputValue(foundItem); + if (advanced) { + let foundItem = findDefaultItem(values, advanced, true); + setInputValue(foundItem); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [advanced, JSON.stringify(schema), JSON.stringify(values)]); + }, [advanced, JSON.stringify(values), JSON.stringify(schema)]); + + useEffect(() => { + if (!advanced) { + let foundItem = findDefaultItem(values, advanced, true); + setInputValue(foundItem); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [advanced, JSON.stringify(values)]); useEffect(() => { if (isInitialRender.current) return; @@ -487,7 +497,12 @@ export const MultiselectV2 = ({ containerRef={valueContainerRef} showAllOption={showAllOption} isSelectAllSelected={isSelectAllSelected} - setIsSelectAllSelected={setIsSelectAllSelected} + setIsSelectAllSelected={(value) => { + setIsSelectAllSelected(value); + if (!value) { + fireEvent('onSelect'); + } + }} setSelected={setInputValue} iconColor={iconColor} optionsLoadingState={optionsLoadingState && advanced} diff --git a/server/src/helpers/components.helper.ts b/server/src/helpers/components.helper.ts index a9aea3403e..f59eed972c 100644 --- a/server/src/helpers/components.helper.ts +++ b/server/src/helpers/components.helper.ts @@ -36,11 +36,13 @@ export const buildComponentMetaDefinition = (components = {}) => { componentMeta.definition.properties, currentComponentData?.component?.definition?.properties, (objValue, srcValue) => { - if ( - ['Table', 'DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) && + if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) { + return srcValue; + } else if ( + ['DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) && isArray(objValue) ) { - return srcValue; + return isArray(srcValue) ? srcValue : Object.values(srcValue); } } ), diff --git a/server/src/services/components.service.ts b/server/src/services/components.service.ts index cbc728a105..1d436a6e72 100644 --- a/server/src/services/components.service.ts +++ b/server/src/services/components.service.ts @@ -97,13 +97,13 @@ export class ComponentsService { componentData[column === 'others' ? 'displayPreferences' : column], updatedDefinition[column], (objValue, srcValue) => { - if ( - (componentData.type === 'Table' || - componentData.type === 'DropdownV2' || - componentData.type === 'MultiselectV2') && + if (componentData.type === 'Table' && _.isArray(objValue)) { + return srcValue; + } else if ( + (componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2') && _.isArray(objValue) ) { - return srcValue; + return _.isArray(srcValue) ? srcValue : Object.values(srcValue); } } ); From 6a4eab9ec6b6ddcf081dcfc6c688a56e76d0e98d Mon Sep 17 00:00:00 2001 From: Shaurya Sharma <79473274+shaurya-sharma064@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:31:06 +0530 Subject: [PATCH 029/104] Arrow key canvas movement fix with auto-alignment popover disappearing fix (#2600) --- .../AppCanvas/AutoComputeMobileLayoutAlert.jsx | 1 + frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx | 13 +++++++------ frontend/src/AppBuilder/_stores/slices/gridSlice.js | 10 +++++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx b/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx index a36209e525..1124c326ad 100644 --- a/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AutoComputeMobileLayoutAlert.jsx @@ -77,6 +77,7 @@ export default function AutoComputeMobileLayoutAlert({ currentLayout, darkMode } padding: 'var(--7, 16px)', background: 'var(--base)', margin: '10px', + zIndex: '1', }} className="d-flex flex-row" > diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 985b62eb89..f5603de836 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -44,7 +44,8 @@ export default function Grid({ gridWidth, currentLayout }) { const isGroupHandleHoverd = useIsGroupHandleHoverd(); const openModalWidgetId = useOpenModalWidgetId(); const moveableRef = useRef(null); - const [triggerCanvasUpdater, setTriggerCanvasUpdater] = useState(false); + const triggerCanvasUpdater = useStore((state) => state.triggerCanvasUpdater, shallow); + const toggleCanvasUpdater = useStore((state) => state.toggleCanvasUpdater, shallow); const groupResizeDataRef = useRef([]); const isDraggingRef = useRef(false); const canvasWidth = NO_OF_GRIDS * gridWidth; @@ -347,7 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [boxList, currentLayout, gridWidth] @@ -488,7 +489,7 @@ export default function Grid({ gridWidth, currentLayout }) { console.error('ResizeEnd error ->', error); } useGridStore.getState().actions.setDragTarget(); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} onResizeStart={(e) => { if (!isComponentVisible(e.target.id)) { @@ -575,7 +576,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error resizing group', error); } - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} checkInput onDragStart={(e) => { @@ -722,7 +723,7 @@ export default function Grid({ gridWidth, currentLayout }) { element.classList.add('hide-grid'); }); document.getElementById('real-canvas')?.classList.remove('show-grid'); - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} onDrag={(e) => { // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again @@ -857,7 +858,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error dragging group', error); } - setTriggerCanvasUpdater((prev) => !prev); + toggleCanvasUpdater(); }} // throttleDrag={1} // edgeDraggable={false} diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index 3a6e3146d5..69e5560497 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -1,7 +1,9 @@ import { NO_OF_GRIDS } from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import { debounce } from 'lodash'; const initialState = { hoveredComponentForGrid: '', + triggerCanvasUpdater: false, }; export const createGridSlice = (set, get) => ({ @@ -9,8 +11,13 @@ export const createGridSlice = (set, get) => ({ setHoveredComponentForGrid: (id) => set(() => ({ hoveredComponentForGrid: id }), false, { type: 'setHoveredComponentForGrid', id }), getHoveredComponentForGrid: () => get().hoveredComponentForGrid, + toggleCanvasUpdater: () => + set((state) => ({ triggerCanvasUpdater: !state.triggerCanvasUpdater }), false, { type: 'toggleCanvasUpdater' }), + debouncedToggleCanvasUpdater: debounce(() => { + get().toggleCanvasUpdater(); + }, 200), moveComponentPosition: (direction) => { - const { setComponentLayout, currentLayout, getSelectedComponentsDefinition } = get(); + const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get(); let layouts = {}; const selectedComponents = getSelectedComponentsDefinition(); selectedComponents.forEach((selectedComponent) => { @@ -53,5 +60,6 @@ export const createGridSlice = (set, get) => ({ }; }); setComponentLayout(layouts); + debouncedToggleCanvasUpdater(); }, }); From 1168ee940913406c11abb0f03c8b5b8f1bf9bead Mon Sep 17 00:00:00 2001 From: Kartik Gupta Date: Tue, 22 Oct 2024 14:54:59 +0530 Subject: [PATCH 030/104] get exposed variables from form.children is element is a form child (#2489) --- .../AppBuilder/_stores/slices/resolvedSlice.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index c83f8408ce..6109176401 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -394,7 +394,22 @@ export const createResolvedSlice = (set, get) => ({ return get().resolvedStore?.modules?.[moduleId]?.components?.[componentId]; }, getExposedValueOfComponent: (componentId, moduleId = 'canvas') => { - return get().resolvedStore.modules[moduleId].exposedValues.components[componentId] || {}; + try { + const components = get().getCurrentPageComponents(); + const { + component: { parent: parentId, name: componentName }, + } = components[componentId]; + if (parentId) { + // if parent is form get exposed values from children + const { component: parentComopnent } = components[parentId]; + if (parentComopnent.component === 'Form') { + return get().resolvedStore.modules[moduleId].exposedValues.components[parentId].children[componentName] || {}; + } + } + return get().resolvedStore.modules[moduleId].exposedValues.components[componentId] || {}; + } catch (error) { + return {}; + } }, getAllExposedValues: (moduleId = 'canvas') => { return get().resolvedStore.modules[moduleId].exposedValues; From e9828a9b55bb44b84d662dadf16ccfb1a23d36a5 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma <79473274+shaurya-sharma064@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:30:46 +0530 Subject: [PATCH 031/104] Fixed modal not opening when inside a tab via show modal csa (#2603) --- frontend/src/AppBuilder/_stores/slices/resolvedSlice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js index 6109176401..4c29226864 100644 --- a/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/resolvedSlice.js @@ -401,8 +401,8 @@ export const createResolvedSlice = (set, get) => ({ } = components[componentId]; if (parentId) { // if parent is form get exposed values from children - const { component: parentComopnent } = components[parentId]; - if (parentComopnent.component === 'Form') { + const { component: parentComopnent } = components?.[parentId] || {}; + if (parentComopnent?.component === 'Form') { return get().resolvedStore.modules[moduleId].exposedValues.components[parentId].children[componentName] || {}; } } From e58180262feefcd1c14cc5edf53d93e7c6922725 Mon Sep 17 00:00:00 2001 From: Shaurya Sharma <79473274+shaurya-sharma064@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:03:54 +0530 Subject: [PATCH 032/104] Fixed resolving array of objects overlap issue (#2613) --- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 63f7e4cb4a..c8a6a09aed 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -1166,6 +1166,8 @@ export const createComponentsSlice = (set, get) => ({ if (Array.isArray(oldValue?.value)) { const resolvedComponent = { [componentId]: deepClone(getResolvedComponent(componentId) ?? {}) }; + resolvedComponent[componentId][paramType][property] = []; + const { updatedValue } = checkValueAndResolve( componentId, paramType, @@ -1176,10 +1178,6 @@ export const createComponentsSlice = (set, get) => ({ true, moduleId ); - resolvedComponent[componentId][paramType][property] = resolvedComponent[componentId][paramType][property].slice( - 0, - value.length - ); setResolvedComponent(componentId, resolvedComponent[componentId], moduleId); // If the value is not changed, return From 2f765e12436d5a4007239c882f526cf8db54c42b Mon Sep 17 00:00:00 2001 From: Shaurya Sharma <79473274+shaurya-sharma064@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:45:58 +0530 Subject: [PATCH 033/104] Fixed dropdown not opening when clicked on input (#2618) * Fixed dropdown not opening when clicked on label * onFocus event fix --- .../Components/DropdownV2/DropdownV2.jsx | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index 8b554ec3c2..da1c28afb3 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -95,11 +95,12 @@ export const DropdownV2 = ({ const [validationStatus, setValidationStatus] = useState(validate(currentValue)); const { isValid, validationError } = validationStatus; const ref = React.useRef(null); + const dropdownRef = React.useRef(null); const [visibility, setVisibility] = useState(properties.visibility); const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState); const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); - const [isFocused, setIsFocused] = useState(false); const [searchInputValue, setSearchInputValue] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; const labelRef = useRef(); function findDefaultItem(schema) { @@ -156,9 +157,14 @@ export const DropdownV2 = ({ const handleOutsideClick = (e) => { let menu = ref.current.querySelector('.select__menu'); if (!ref.current.contains(e.target) || !menu || !menu.contains(e.target)) { - setIsFocused(false); setSearchInputValue(''); } + if (dropdownRef.current && !dropdownRef.current?.contains(e.target) && !menu && !menu?.contains(e.target)) { + if (isDropdownOpen) { + fireEvent('onBlur'); + } + setIsDropdownOpen(false); + } }; const setInputValue = (value) => { @@ -192,7 +198,7 @@ export const DropdownV2 = ({ return () => { document.removeEventListener('mousedown', handleOutsideClick); }; - }, []); + }, [isDropdownOpen]); useEffect(() => { if (visibility !== properties.visibility) setVisibility(properties.visibility); @@ -407,6 +413,7 @@ export const DropdownV2 = ({ return ( <>
    -
    +
    { + if (!isDropdownDisabled) { + fireEvent('onFocus'); + setIsDropdownOpen((prev) => !prev); + } + }} + ref={ref} + >