This commit is contained in:
proelements
2026-05-04 15:07:06 +03:00
parent 872bc6fb57
commit 741540b767
148 changed files with 11063 additions and 1016 deletions
@@ -0,0 +1,287 @@
import { useState } from 'react';
import {
Alert,
Box,
Checkbox,
Chip,
FormControlLabel,
Link,
Stack,
Switch,
Tooltip,
Typography,
} from '@elementor/ui';
import { AlertTriangleFilledIcon, ExternalLinkIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
import * as PropTypes from 'prop-types';
const SubSettingRow = ( {
label,
checked,
onChange,
disabled = false,
limitExceeded = false,
overLimitCount = 0,
onReviewClick,
overrideAll = false,
onOverrideAllChange,
showOverrideOption = false,
notExported = false,
} ) => {
if ( notExported ) {
return (
<Box
sx={ {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 1.25,
} }
>
<Typography variant="body1" color="text.primary">
{ label }
</Typography>
<Typography variant="body1" color="text.secondary">
{ __( 'Not exported', 'elementor' ) }
</Typography>
</Box>
);
}
return (
<Box
sx={ {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 1.25,
} }
>
<Stack direction="row" alignItems="center" spacing={ 1 } sx={ { flex: 1 } }>
<Typography variant="body1" color="text.primary">
{ label }
</Typography>
{ limitExceeded && overLimitCount > 0 && (
<Chip
label={ `${ overLimitCount } ${ __( 'over limit', 'elementor' ) }` }
size="tiny"
sx={ {
height: 20,
borderColor: 'warning.main',
color: 'warning.main',
backgroundColor: 'transparent',
'& .MuiChip-label': {
px: 0.75,
fontSize: 12,
},
} }
variant="outlined"
/>
) }
{ limitExceeded && onReviewClick && (
<Link
component="button"
variant="body2"
color="info.main"
onClick={ onReviewClick }
sx={ {
display: 'flex',
alignItems: 'center',
gap: 0.5,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
} }
>
{ __( 'Review', 'elementor' ) }
<ExternalLinkIcon sx={ { fontSize: 16 } } />
</Link>
) }
</Stack>
<Stack direction="row" alignItems="center" spacing={ 1 }>
{ showOverrideOption && limitExceeded && (
<Stack direction="row" alignItems="center" spacing={ 0.5 }>
<FormControlLabel
control={
<Checkbox
checked={ overrideAll }
onChange={ ( e ) => onOverrideAllChange?.( e.target.checked ) }
color="info"
size="small"
sx={ { p: 0 } }
/>
}
label={ __( 'Override all', 'elementor' ) }
sx={ {
gap: 1,
mr: 0,
'& .MuiFormControlLabel-label': {
fontSize: 14,
},
} }
/>
<Tooltip
title={ __( 'This will delete all existing items and replace them with the imported ones', 'elementor' ) }
placement="top"
arrow
>
<AlertTriangleFilledIcon
sx={ {
fontSize: 16,
color: 'warning.main',
cursor: 'pointer',
} }
/>
</Tooltip>
</Stack>
) }
<Switch
checked={ checked }
onChange={ ( e, isChecked ) => onChange?.( isChecked ) }
color="info"
size="medium"
disabled={ disabled || ( limitExceeded && ! overrideAll ) }
/>
</Stack>
</Box>
);
};
SubSettingRow.propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func,
disabled: PropTypes.bool,
limitExceeded: PropTypes.bool,
overLimitCount: PropTypes.number,
onReviewClick: PropTypes.func,
overrideAll: PropTypes.bool,
onOverrideAllChange: PropTypes.func,
showOverrideOption: PropTypes.bool,
notExported: PropTypes.bool,
};
export function ClassesVariablesSection( {
settings,
onSettingChange,
isImport = false,
classesExported = true,
variablesExported = true,
classesLimitExceeded = false,
variablesLimitExceeded = false,
classesOverLimitCount = 0,
variablesOverLimitCount = 0,
onClassesReviewClick,
onVariablesReviewClick,
disabled = false,
notExported = false,
} ) {
const [ classesOverrideAll, setClassesOverrideAll ] = useState( settings.classesOverrideAll ?? false );
const [ variablesOverrideAll, setVariablesOverrideAll ] = useState( settings.variablesOverrideAll ?? false );
const hasLimitWarning = isImport && ( classesLimitExceeded || variablesLimitExceeded );
const classesNotExported = isImport && ! classesExported;
const variablesNotExported = isImport && ! variablesExported;
return (
<Box sx={ { mb: 3, border: 1, borderRadius: 1, borderColor: 'action.focus', p: 2.5 } }>
<Stack spacing={ 2.5 }>
<Box sx={ { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }>
<Typography variant="h6">
{ __( 'Classes & variables', 'elementor' ) }
</Typography>
</Box>
{ hasLimitWarning && ! notExported && (
<Alert
severity="warning"
icon={ <AlertTriangleFilledIcon sx={ { color: 'warning.main' } } /> }
sx={ {
alignItems: 'center',
backgroundColor: 'warning.background',
'& .MuiAlert-message': {
display: 'flex',
alignItems: 'center',
gap: 0.75,
},
} }
>
<Typography
variant="body2"
component="span"
sx={ { fontWeight: 500 } }
color="text.secondary"
>
{ __( 'Import limit reached.', 'elementor' ) }
</Typography>
<Typography variant="body2" component="span" color="text.secondary">
{ __( 'To resolve this, review existing items or choose to override', 'elementor' ) }
</Typography>
</Alert>
) }
<Stack spacing={ 1.5 }>
<SubSettingRow
label={ __( 'Classes', 'elementor' ) }
checked={ settings.classes ?? false }
onChange={ ( isChecked ) => onSettingChange( 'classes', isChecked ) }
disabled={ disabled }
limitExceeded={ isImport && classesLimitExceeded && ! classesNotExported }
overLimitCount={ classesOverLimitCount }
onReviewClick={ onClassesReviewClick }
overrideAll={ classesOverrideAll }
onOverrideAllChange={ ( checked ) => {
setClassesOverrideAll( checked );
onSettingChange( 'classesOverrideAll', checked );
} }
showOverrideOption={ isImport && ! classesNotExported }
notExported={ classesNotExported }
/>
<SubSettingRow
label={ __( 'Variables', 'elementor' ) }
checked={ settings.variables ?? false }
onChange={ ( isChecked ) => onSettingChange( 'variables', isChecked ) }
disabled={ disabled }
limitExceeded={ isImport && variablesLimitExceeded && ! variablesNotExported }
overLimitCount={ variablesOverLimitCount }
onReviewClick={ onVariablesReviewClick }
overrideAll={ variablesOverrideAll }
onOverrideAllChange={ ( checked ) => {
setVariablesOverrideAll( checked );
onSettingChange( 'variablesOverrideAll', checked );
} }
showOverrideOption={ isImport && ! variablesNotExported }
notExported={ variablesNotExported }
/>
</Stack>
</Stack>
</Box>
);
}
ClassesVariablesSection.propTypes = {
settings: PropTypes.shape( {
classes: PropTypes.bool,
variables: PropTypes.bool,
classesOverrideAll: PropTypes.bool,
variablesOverrideAll: PropTypes.bool,
} ).isRequired,
onSettingChange: PropTypes.func.isRequired,
isImport: PropTypes.bool,
classesExported: PropTypes.bool,
variablesExported: PropTypes.bool,
classesLimitExceeded: PropTypes.bool,
variablesLimitExceeded: PropTypes.bool,
classesOverLimitCount: PropTypes.number,
variablesOverLimitCount: PropTypes.number,
onClassesReviewClick: PropTypes.func,
onVariablesReviewClick: PropTypes.func,
disabled: PropTypes.bool,
notExported: PropTypes.bool,
};
@@ -0,0 +1,120 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
Stack,
Typography,
} from '@elementor/ui';
import { AlertTriangleFilledIcon, XIcon } from '@elementor/icons';
import { __ } from '@wordpress/i18n';
import * as PropTypes from 'prop-types';
const getDialogContent = ( type ) => {
switch ( type ) {
case 'both':
return {
title: __( 'Override all classes and variables?', 'elementor' ),
description: __( 'This will delete all existing classes and variables and replace them with the imported ones. This action cannot be undone.', 'elementor' ),
};
case 'variables':
return {
title: __( 'Override all variables?', 'elementor' ),
description: __( 'This will delete all existing variables and replace them with the imported ones. This action cannot be undone.', 'elementor' ),
};
case 'classes':
default:
return {
title: __( 'Override all classes?', 'elementor' ),
description: __( 'This will delete all existing classes and replace them with the imported ones. This action cannot be undone.', 'elementor' ),
};
}
};
export function OverrideConfirmationDialog( {
open,
onClose,
onConfirm,
type = 'classes',
} ) {
const { title, description } = getDialogContent( type );
return (
<Dialog
open={ open }
onClose={ onClose }
maxWidth="xs"
fullWidth
>
<DialogContent sx={ { pt: 2, pb: 1.5 } }>
<Stack spacing={ 1.5 }>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Stack direction="row" spacing={ 1.5 } alignItems="flex-start" sx={ { flex: 1 } }>
<AlertTriangleFilledIcon
sx={ {
color: 'warning.dark',
fontSize: 24,
flexShrink: 0,
} }
/>
<Typography
variant="subtitle1"
sx={ { fontWeight: 500 } }
>
{ title }
</Typography>
</Stack>
<Button
onClick={ onClose }
sx={ {
minWidth: 'auto',
p: 0.5,
color: 'text.primary',
} }
>
<XIcon sx={ { fontSize: 20 } } />
</Button>
</Stack>
<Typography
variant="body2"
color="text.secondary"
sx={ { pr: 1 } }
>
{ description }
</Typography>
</Stack>
</DialogContent>
<DialogActions sx={ { px: 3, pb: 2 } }>
<Button
onClick={ onClose }
color="secondary"
variant="text"
>
{ __( 'Cancel', 'elementor' ) }
</Button>
<Button
onClick={ onConfirm }
variant="contained"
sx={ {
color: 'white',
backgroundColor: 'warning.main',
'&:hover': {
backgroundColor: 'warning.dark',
},
} }
>
{ __( 'Save and override', 'elementor' ) }
</Button>
</DialogActions>
</Dialog>
);
}
OverrideConfirmationDialog.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
type: PropTypes.oneOf( [ 'classes', 'variables', 'both' ] ),
};
@@ -0,0 +1,95 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useTabFocus } from './use-tab-focus';
const DEFAULT_CLASSES_LIMIT = 100;
const DEFAULT_VARIABLES_LIMIT = 100;
function getLimitsFromConfig() {
const config = window.elementorAppConfig?.[ 'import-export-customization' ];
return {
classes: config?.limits?.classes ?? DEFAULT_CLASSES_LIMIT,
variables: config?.limits?.variables ?? DEFAULT_VARIABLES_LIMIT,
};
}
export function useClassesVariablesLimits( { open, isImport } ) {
const [ existingClassesCount, setExistingClassesCount ] = useState( 0 );
const [ existingVariablesCount, setExistingVariablesCount ] = useState( 0 );
const [ isLoading, setIsLoading ] = useState( false );
const [ error, setError ] = useState( null );
const limits = useMemo( () => getLimitsFromConfig(), [] );
const fetchCounts = useCallback( async () => {
if ( ! open || ! isImport ) {
return;
}
setIsLoading( true );
setError( null );
try {
const baseUrl = window.wpApiSettings?.root || '/wp-json/';
const nonce = window.wpApiSettings?.nonce || '';
const [ classesResponse, variablesResponse ] = await Promise.all( [
fetch( `${ baseUrl }elementor/v1/global-classes`, {
headers: {
'X-WP-Nonce': nonce,
},
} ),
fetch( `${ baseUrl }elementor/v1/variables/list`, {
headers: {
'X-WP-Nonce': nonce,
},
} ),
] );
if ( classesResponse.ok ) {
const classesData = await classesResponse.json();
const classesCount = Object.keys( classesData?.data || {} ).length;
setExistingClassesCount( classesCount );
}
if ( variablesResponse.ok ) {
const variablesData = await variablesResponse.json();
const variablesCount = variablesData?.data?.total || 0;
setExistingVariablesCount( variablesCount );
}
} catch ( err ) {
setError( err );
} finally {
setIsLoading( false );
}
}, [ open, isImport ] );
useEffect( () => {
fetchCounts();
}, [ fetchCounts ] );
useTabFocus( fetchCounts );
const calculateLimitInfo = useCallback( ( existingCount, importedCount, limit ) => {
const totalAfterImport = existingCount + importedCount;
const isExceeded = totalAfterImport > limit;
const overLimitCount = isExceeded ? totalAfterImport - limit : 0;
return {
isExceeded,
overLimitCount,
totalAfterImport,
};
}, [] );
return {
existingClassesCount,
existingVariablesCount,
classesLimit: limits.classes,
variablesLimit: limits.variables,
isLoading,
error,
calculateLimitInfo,
refetch: fetchCounts,
};
}
@@ -0,0 +1,13 @@
import { useEffect } from 'react';
export function useTabFocus( callback ) {
useEffect( () => {
const handleVisibilityChange = () => {
if ( 'visible' === document.visibilityState ) {
callback();
}
};
document.addEventListener( 'visibilitychange', handleVisibilityChange );
return () => document.removeEventListener( 'visibilitychange', handleVisibilityChange );
}, [ callback ] );
}
@@ -0,0 +1,20 @@
<?php
namespace ElementorPro\Core\App\Modules\ImportExportCustomization;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ElementorPro\License\API;
class Utils {
public static function is_high_tier(): bool {
try {
$plan_type = API::get_plan_type();
return 'expert' === $plan_type || 'agency' === $plan_type;
} catch ( \Exception $exception ) {
return false;
}
}
}