mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
* test: verify pre-commit hook * fix: clean up code formatting and improve readability across multiple components * chore: update subproject commit reference in frontend/ee * chore: update eslint to version 9.26.0 and remove unused dependencies from package.json fix: update submodule reference in server/ee * chore: refactor ESLint configuration and add quiet linting script; update components to disable specific ESLint rules * chore: add GitHub Copilot review instructions for App Builder team Covers backward compatibility rules, styling conventions, state management, resolution system, widget definitions, and common review flags. * chore: add review instructions for App Builder, Data Migrations, Server Widget Config, Widget Components, and Widget Config * Enhance TypeScript support in frontend configuration - Added TypeScript parser and linting rules to ESLint configuration. - Updated Babel configuration to include TypeScript preset. - Modified package.json and package-lock.json to include TypeScript and related dependencies. - Introduced tsconfig.json for TypeScript compiler options. - Updated Webpack configuration to support .ts and .tsx file extensions. - Adjusted linting and formatting scripts to include TypeScript files. * chore: update TypeScript ESLint packages and subproject commits --------- Co-authored-by: kavinvenkatachalam <kavin.saratha@gmail.com> Co-authored-by: Johnson Cherian <johnsonc.dev@gmail.com>
401 lines
15 KiB
JavaScript
401 lines
15 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { appVersionService, authenticationService, gitSyncService } from '@/_services';
|
|
import AlertDialog from '@/_ui/AlertDialog';
|
|
import { Alert } from '@/_ui/Alert';
|
|
import { toast } from 'react-hot-toast';
|
|
import { useTranslation } from 'react-i18next';
|
|
import Select from '@/_ui/Select';
|
|
import { shallow } from 'zustand/shallow';
|
|
import useStore from '@/AppBuilder/_stores/store';
|
|
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
|
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
|
import '../../_styles/version-modal.scss';
|
|
|
|
const CreateVersionModal = ({
|
|
showCreateAppVersion,
|
|
setShowCreateAppVersion,
|
|
handleCommitEnableChange,
|
|
canCommit,
|
|
orgGit,
|
|
fetchingOrgGit,
|
|
handleCommitOnVersionCreation = () => {},
|
|
versionId,
|
|
onVersionCreated,
|
|
}) => {
|
|
const { moduleId } = useModuleContext();
|
|
const setResolvedGlobals = useStore((state) => state.setResolvedGlobals, shallow);
|
|
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
|
const [versionName, setVersionName] = useState('');
|
|
const [versionDescription, setVersionDescription] = useState('');
|
|
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
|
|
const {
|
|
changeEditorVersionAction,
|
|
environmentChangedAction,
|
|
fetchDevelopmentVersions,
|
|
developmentVersions,
|
|
appId,
|
|
selectedVersion,
|
|
currentMode,
|
|
currentEnvironment,
|
|
environments,
|
|
setIsEditorFreezed,
|
|
} = useStore(
|
|
(state) => ({
|
|
changeEditorVersionAction: state.changeEditorVersionAction,
|
|
environmentChangedAction: state.environmentChangedAction,
|
|
selectedEnvironment: state.selectedEnvironment,
|
|
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
|
developmentVersions: state.developmentVersions,
|
|
featureAccess: state.license.featureAccess,
|
|
editingVersion: state.currentVersionId,
|
|
appId: state.appId ?? state.appStore.modules[moduleId]?.app?.appId,
|
|
currentVersionId: state.currentVersionId,
|
|
selectedVersion: state.selectedVersion,
|
|
currentMode: state.currentMode,
|
|
currentEnvironment: state.selectedEnvironment,
|
|
environments: state.environments,
|
|
setIsEditorFreezed: state.setIsEditorFreezed,
|
|
}),
|
|
shallow
|
|
);
|
|
|
|
const [selectedVersionForCreation, setSelectedVersionForCreation] = useState(null);
|
|
const textareaRef = React.useRef(null);
|
|
|
|
const handleDescriptionInput = (e) => {
|
|
const textarea = textareaRef.current;
|
|
if (!textarea) return;
|
|
textarea.style.height = 'auto';
|
|
const lineHeight = 24;
|
|
const maxLines = 4;
|
|
const maxHeight = lineHeight * maxLines;
|
|
textarea.style.height = Math.min(textarea.scrollHeight, maxHeight) + 'px';
|
|
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (appId) {
|
|
fetchDevelopmentVersions(appId);
|
|
}
|
|
}, [appId, fetchDevelopmentVersions]);
|
|
|
|
// Set the version to promote when modal opens or when developmentVersions/versionId changes
|
|
useEffect(() => {
|
|
// Only run when modal is open
|
|
if (!showCreateAppVersion) {
|
|
return;
|
|
}
|
|
|
|
// Wait for developmentVersions to be loaded
|
|
if (!developmentVersions?.length) {
|
|
return;
|
|
}
|
|
|
|
// If versionId prop is provided, ONLY use that specific version
|
|
if (versionId) {
|
|
const versionToPromote = developmentVersions.find((version) => version?.id === versionId);
|
|
if (versionToPromote) {
|
|
setSelectedVersionForCreation(versionToPromote);
|
|
setVersionName(versionToPromote.name);
|
|
setVersionDescription(versionToPromote.description || '');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If no versionId prop, use selectedVersion from store
|
|
if (selectedVersion?.id) {
|
|
const selected = developmentVersions.find((version) => version?.id === selectedVersion?.id);
|
|
if (selected) {
|
|
setSelectedVersionForCreation(selected);
|
|
setVersionName(selected.name);
|
|
setVersionDescription(selected.description || '');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback: if no version is selected or found, use the first development version
|
|
if (developmentVersions.length > 0) {
|
|
setSelectedVersionForCreation(developmentVersions[0]);
|
|
setVersionName(developmentVersions[0].name);
|
|
setVersionDescription(developmentVersions[0].description || '');
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [developmentVersions, versionId, showCreateAppVersion]);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const createVersion = async () => {
|
|
if (versionName.trim().length > 25) {
|
|
toast.error('Version name should not be longer than 25 characters');
|
|
return;
|
|
}
|
|
if (versionDescription.trim().length > 500) {
|
|
toast.error('Version description should not be longer than 500 characters');
|
|
return;
|
|
}
|
|
if (versionName.trim() == '') {
|
|
toast.error('Version name should not be empty');
|
|
return;
|
|
}
|
|
|
|
if (!selectedVersionForCreation) {
|
|
toast.error('Please wait while versions are loading...');
|
|
return;
|
|
}
|
|
|
|
setIsCreatingVersion(true);
|
|
|
|
try {
|
|
await appVersionService.save(appId, selectedVersionForCreation.id, {
|
|
name: versionName,
|
|
description: versionDescription,
|
|
// need to add commit changes logic here
|
|
status: 'PUBLISHED',
|
|
});
|
|
toast.success('Version Created successfully');
|
|
setVersionName('');
|
|
setVersionDescription('');
|
|
setSelectedVersionForCreation(null);
|
|
setIsCreatingVersion(false);
|
|
setShowCreateAppVersion(false);
|
|
|
|
// Fetch versions after creation
|
|
if (onVersionCreated) {
|
|
onVersionCreated();
|
|
}
|
|
// Refresh development versions to update the lock status
|
|
fetchDevelopmentVersions(appId);
|
|
// Switch to the newly created published version properly
|
|
// The newly created version will be in the draft's environment (development)
|
|
// but with PUBLISHED status. We may need to switch environment first.
|
|
try {
|
|
const newVersionData = await appVersionService.getAppVersionData(
|
|
appId,
|
|
selectedVersionForCreation.id,
|
|
currentMode
|
|
);
|
|
|
|
// Set editor freeze state based on should_freeze_editor
|
|
if (newVersionData.should_freeze_editor !== undefined) {
|
|
setIsEditorFreezed(newVersionData.should_freeze_editor);
|
|
}
|
|
|
|
if (newVersionData.editing_version?.id) {
|
|
const newVersionEnvironmentId = newVersionData.editing_version.currentEnvironmentId;
|
|
const isDifferentEnvironment = newVersionEnvironmentId !== currentEnvironment?.id;
|
|
|
|
if (isDifferentEnvironment) {
|
|
// Need to switch environment first, then switch to the version
|
|
const targetEnvironment = environments.find((env) => env.id === newVersionEnvironmentId);
|
|
if (targetEnvironment) {
|
|
// First switch environment
|
|
await environmentChangedAction(targetEnvironment, () => {
|
|
// Then switch to the new version
|
|
changeEditorVersionAction(
|
|
appId,
|
|
newVersionData.editing_version.id,
|
|
() => {
|
|
console.log('Successfully switched environment and version');
|
|
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
|
},
|
|
(error) => {
|
|
console.error('Error switching to newly created version:', error);
|
|
toast.error('Version created but failed to switch to it');
|
|
}
|
|
);
|
|
});
|
|
}
|
|
} else {
|
|
// Same environment, just switch version
|
|
await changeEditorVersionAction(
|
|
appId,
|
|
newVersionData.editing_version.id,
|
|
() => {
|
|
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
|
},
|
|
(error) => {
|
|
console.error('Error switching to newly created version:', error);
|
|
toast.error('Version created but failed to switch to it');
|
|
}
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting new version data:', error);
|
|
toast.error('Version created but failed to switch to it');
|
|
}
|
|
} catch (error) {
|
|
if (error?.data?.code === '23505') {
|
|
toast.error('Version name already exists.');
|
|
} else if (error?.error) {
|
|
toast.error(error?.error);
|
|
} else {
|
|
toast.error('Error while creating version. Please try again.');
|
|
}
|
|
} finally {
|
|
setIsCreatingVersion(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AlertDialog
|
|
show={showCreateAppVersion}
|
|
closeModal={() => {
|
|
setVersionName('');
|
|
setVersionDescription('');
|
|
setSelectedVersionForCreation(null);
|
|
setShowCreateAppVersion(false);
|
|
}}
|
|
title={'Save version'}
|
|
customClassName="create-version-modal"
|
|
>
|
|
{fetchingOrgGit ? (
|
|
<div className="loader-container">
|
|
<div className="primary-spin-loader"></div>
|
|
</div>
|
|
) : (
|
|
<form
|
|
className="create-version-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
createVersion();
|
|
}}
|
|
>
|
|
<div className="create-version-body mb-3">
|
|
<div className="col">
|
|
<label className="form-label mb-1 ms-1" data-cy="version-name-label">
|
|
{t('editor.appVersionManager.versionName', 'Version Name')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
onChange={(e) => setVersionName(e.target.value)}
|
|
className="form-control"
|
|
data-cy="version-name-input-field"
|
|
placeholder={t('editor.appVersionManager.enterVersionName', 'Enter version name')}
|
|
disabled={isCreatingVersion}
|
|
value={versionName}
|
|
autoFocus={true}
|
|
minLength="1"
|
|
maxLength="25"
|
|
/>
|
|
<small className="version-name-helper-text" data-cy="version-name-helper-text">
|
|
{t('editor.appVersionManager.versionNameHelper', 'Version name must be unique and max 25 characters')}
|
|
</small>
|
|
</div>
|
|
<div className="col mt-2">
|
|
<label className="form-label mb-1 ms-1" data-cy="version-description-label">
|
|
{t('editor.appVersionManager.versionDescription', 'Version description')}
|
|
</label>
|
|
<textarea
|
|
type="text"
|
|
ref={textareaRef}
|
|
onInput={handleDescriptionInput}
|
|
onChange={(e) => setVersionDescription(e.target.value)}
|
|
className="form-control app-version-description"
|
|
data-cy="version-description-input-field"
|
|
placeholder={t('editor.appVersionManager.enterVersionDescription', 'Enter version description')}
|
|
disabled={isCreatingVersion}
|
|
value={versionDescription}
|
|
autoFocus={true}
|
|
minLength="0"
|
|
maxLength="500"
|
|
rows={1}
|
|
/>
|
|
<small className="version-description-helper-text" data-cy="version-description-helper-text">
|
|
{t('editor.appVersionManager.versionDescriptionHelper', 'Description must be max 500 characters')}
|
|
</small>
|
|
</div>
|
|
|
|
{/* <div className="mb-4 pb-2 version-select">
|
|
<label className="form-label" data-cy="create-version-from-label">
|
|
{t('editor.appVersionManager.createVersionFrom', 'Create version from')}
|
|
</label>
|
|
<div className="ts-control" data-cy="create-version-from-input-field">
|
|
<Select
|
|
options={options}
|
|
value={selectedVersionForCreation}
|
|
onChange={(version) => {
|
|
setSelectedVersionForCreation(version);
|
|
}}
|
|
useMenuPortal={false}
|
|
width="100%"
|
|
maxMenuHeight={100}
|
|
/>
|
|
</div>
|
|
</div> */}
|
|
|
|
{isGitSyncEnabled && (
|
|
<div className="commit-changes mt-3">
|
|
<div>
|
|
<input
|
|
className="form-check-input"
|
|
checked={canCommit}
|
|
type="checkbox"
|
|
onChange={handleCommitEnableChange}
|
|
data-cy="git-commit-input"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
|
Commit changes
|
|
</div>
|
|
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
|
This will commit the creation of the new version to the git repo
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="mt-3">
|
|
<Alert placeSvgTop={true} svg="warning-icon" className="create-version-alert">
|
|
<div
|
|
className="d-flex align-items-center"
|
|
style={{
|
|
justifyContent: 'space-between',
|
|
flexWrap: 'wrap',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<div className="create-version-helper-text" data-cy="create-version-helper-text">
|
|
Saving the version will lock it. To make any edits afterwards, you'll need to create a draft
|
|
version.
|
|
</div>
|
|
</div>
|
|
</Alert>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="create-version-footer">
|
|
<hr className="section-divider" style={{ marginLeft: '-1.5rem', marginRight: '-1.5rem' }} />
|
|
<div className="col d-flex justify-content-end">
|
|
<ButtonSolid
|
|
size="lg"
|
|
onClick={() => {
|
|
setVersionName('');
|
|
setVersionDescription('');
|
|
setShowCreateAppVersion(false);
|
|
}}
|
|
variant="tertiary"
|
|
className="mx-2"
|
|
data-cy="create-version-cancel-button"
|
|
>
|
|
{t('globals.cancel', 'Cancel')}
|
|
</ButtonSolid>
|
|
<ButtonSolid
|
|
size="lg"
|
|
variant="primary"
|
|
className=""
|
|
type="submit"
|
|
disabled={!selectedVersionForCreation || isCreatingVersion}
|
|
data-cy="create-version-save-button"
|
|
>
|
|
{t('editor.appVersionManager.saveVersion', 'Save version')}
|
|
</ButtonSolid>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</AlertDialog>
|
|
);
|
|
};
|
|
|
|
export default CreateVersionModal;
|