diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx index df3a9bcac3..744b24aa67 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/SwitchEditorCard.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Card } from '@openedx/paragon'; import PropTypes from 'prop-types'; import { thunkActions } from '@src/editors/data/redux'; @@ -15,6 +15,7 @@ const SwitchEditorCard = ({ editorType, problemType, }) => { + const intl = useIntl(); const [isConfirmOpen, setConfirmOpen] = React.useState(false); const dispatch = useDispatch(); const { editorRef } = useProblemEditorContext(); @@ -27,7 +28,7 @@ const SwitchEditorCard = ({ close={() => { setConfirmOpen(false); }} - title={} + title={intl.formatMessage(messages[`ConfirmSwitchMessageTitle-${editorType}`])} confirmAction={ - } - isOpen={isOpen} - title={intl.formatMessage(messages.titleLabel)} - > - - - - -
-
-
- -
-
-
- - -
-
- - ); -}; - -ImageSettingsModal.propTypes = { - close: PropTypes.func.isRequired, - isOpen: PropTypes.bool.isRequired, - returnToSelection: PropTypes.func.isRequired, - saveToEditor: PropTypes.func.isRequired, - selection: PropTypes.shape({ - altText: PropTypes.string, - externalUrl: PropTypes.string, - url: PropTypes.string, - }).isRequired, -}; -export default ImageSettingsModal; diff --git a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.test.tsx b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.test.tsx index a55a365b3c..235946d1b4 100644 --- a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.test.tsx +++ b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.test.tsx @@ -1,27 +1,9 @@ +import { initializeMocks, render, screen } from '@src/testUtils'; +import { fireEvent } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; import React from 'react'; -import { render, screen, initializeMocks } from '@src/testUtils'; import ImageSettingsModal from '.'; - -jest.mock('./AltTextControls', () => 'AltTextControls'); -jest.mock('./DimensionControls', () => 'DimensionControls'); - -jest.mock('./hooks', () => ({ - altTextHooks: () => ({ - error: { - show: true, - dismiss: jest.fn(), - }, - isDecorative: false, - value: 'alternative Taxes', - }), - dimensionHooks: () => ({ - onImgLoad: jest.fn( - (selection) => ({ 'hooks.dimensions.onImgLoad.callback': { selection } }), - ).mockName('hooks.dimensions.onImgLoad'), - value: { width: 12, height: 13 }, - }), - onSaveClick: (args) => ({ 'hooks.onSaveClick': args }), -})); +import messages from './messages'; describe('ImageSettingsModal', () => { const props = { @@ -30,16 +12,132 @@ describe('ImageSettingsModal', () => { altText: 'AlTTExt', externalUrl: 'ExtERNALurL', url: 'UrL', + classList: [], }, close: jest.fn().mockName('props.close'), returnToSelection: jest.fn().mockName('props.returnToSelector'), saveToEditor: jest.fn().mockName('props.saveToEditor'), }; + const user = userEvent.setup(); beforeEach(() => { initializeMocks(); }); - test('renders component', () => { - render(); + test('Test null altText', () => { + render(); expect(screen.getByText('Image Settings')).toBeInTheDocument(); }); + test('Test clicking replace image', async () => { + render(); + await user.click(screen.getByRole('button', { name: /replace image/i })); + expect(props.returnToSelection).toHaveBeenCalled(); + }); + test('Test clicking cancel', async () => { + render(); + await user.click(screen.getByRole('button', { name: /cancel/i })); + expect(props.close).toHaveBeenCalled(); + }); + describe('Alt Text Editing', () => { + test('Empty Alt Text raises error if image is nto decorative', async () => { + render(); + await user.clear(screen.getByRole('textbox', { name: /alt text/i })); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByText(messages.altTextLocalFeedback.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.altTextError.defaultMessage)).toBeInTheDocument(); + expect(props.saveToEditor).not.toHaveBeenCalled(); + }); + test('Error can be dismissed', async () => { + render(); + await user.clear(screen.getByRole('textbox', { name: /alt text/i })); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.getByText(messages.altTextError.defaultMessage)).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Dismiss' })); + expect(screen.queryByText(messages.altTextError.defaultMessage)).not.toBeInTheDocument(); + }); + test('Empty Alt Text doesn\'t raise error if image is decorative', async () => { + render(); + await user.clear(screen.getByRole('textbox', { name: /alt text/i })); + await user.click(screen.getByRole('checkbox', { name: /decorative/i })); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.queryByText(messages.altTextLocalFeedback.defaultMessage)).not.toBeInTheDocument(); + expect(screen.queryByText(messages.altTextError.defaultMessage)).not.toBeInTheDocument(); + expect(props.saveToEditor).toHaveBeenCalled(); + }); + test('If alt text is entered it does not raise any error', async () => { + render(); + await user.type(screen.getByRole('textbox', { name: /alt text/i }), 'some text'); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.queryByText(messages.altTextLocalFeedback.defaultMessage)).not.toBeInTheDocument(); + expect(screen.queryByText(messages.altTextError.defaultMessage)).not.toBeInTheDocument(); + expect(props.saveToEditor).toHaveBeenCalled(); + }); + }); + describe('Image Dimensions Editing', () => { + function mockImageLoad(naturalWidth: number, naturalHeight: number) { + const img = screen.getByRole('img'); + Object.defineProperty(img, 'naturalWidth', { value: naturalWidth }); + Object.defineProperty(img, 'naturalHeight', { value: naturalHeight }); + fireEvent.load(img); + } + + test('Image dimensions are editable and saved correctly', async () => { + render(); + mockImageLoad(1920, 1080); + await user.type(await screen.findByRole('textbox', { name: /width/i }), '1280'); + await user.type(screen.getByRole('textbox', { name: /height/i }), '720'); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.queryByText(messages.dimensionLocalFeedback.defaultMessage)).not.toBeInTheDocument(); + expect(props.saveToEditor).toHaveBeenCalled(); + }); + + test('Image dimensions are editable and saved correctly with percentages', async () => { + render(); + mockImageLoad(1920, 1080); + await user.type(await screen.findByRole('textbox', { name: /width/i }), '75%'); + await user.click(screen.getByRole('button', { name: 'Save' })); + expect(screen.queryByText(messages.dimensionLocalFeedback.defaultMessage)).not.toBeInTheDocument(); + expect(props.saveToEditor).toHaveBeenCalled(); + }); + + describe('Dimension Locking', () => { + test('When dimensions are locked it maintains the original ratio', async () => { + render(); + mockImageLoad(1920, 1080); + const widthInput = await screen.findByRole('textbox', { name: /width/i }); + const heightInput = await screen.findByRole('textbox', { name: /height/i }); + await user.clear(widthInput); + await user.type(widthInput, '1280'); + expect(heightInput).toHaveValue('720'); + await user.clear(heightInput); + await user.type(heightInput, '900'); + expect(widthInput).toHaveValue('1600'); + }); + + test('When dimensions are locked it maintains the original ratio with percentages', async () => { + render(); + mockImageLoad(1920, 1080); + const widthInput = await screen.findByRole('textbox', { name: /width/i }); + const heightInput = await screen.findByRole('textbox', { name: /height/i }); + await user.clear(widthInput); + await user.type(widthInput, '75%'); + expect(heightInput).toHaveValue('75%'); + await user.clear(heightInput); + await user.type(heightInput, '90%'); + expect(widthInput).toHaveValue('90%'); + }); + + test('When dimensions are not locked it allows any ratio', async () => { + render(); + mockImageLoad(1920, 1080); + await user.click(await screen.findByRole('button', { name: /unlock dimensions/i })); + const widthInput = await screen.findByRole('textbox', { name: /width/i }); + const heightInput = await screen.findByRole('textbox', { name: /height/i }); + await user.clear(widthInput); + await user.type(widthInput, '1280'); + expect(heightInput).toHaveValue('1080'); + await user.clear(heightInput); + await user.type(heightInput, '900'); + expect(widthInput).toHaveValue('1280'); + }); + }); + }); }); diff --git a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.tsx b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.tsx new file mode 100644 index 0000000000..35abc716c1 --- /dev/null +++ b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/index.tsx @@ -0,0 +1,224 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import './index.scss'; + +import { Button, Image } from '@openedx/paragon'; +import { ArrowBackIos } from '@openedx/paragon/icons'; +import { ImageAdditionalSettingsSlot } from '@src/plugin-slots/ImageAdditionalSettingsSlot'; +import ImageSettingsModalSlot from '@src/plugin-slots/ImageSettingsModalSlot'; +import { Formik, FormikBag, useFormikContext } from 'formik'; +import React from 'react'; +import * as Yup from 'yup'; +import BaseModal from '../../BaseModal'; +import ErrorAlert from '../../ErrorAlerts/ErrorAlert'; +import AltTextControls from './AltTextControls'; +import DimensionControls from './DimensionControls'; +import messages from './messages'; +import type { + HTMLImageAttrs, + ImageConfig, + OrigImageDimensions, +} from './types'; + +interface ImageSettingsModalCommonProps { + close: () => void; + isOpen: boolean; + returnToSelection: () => void; + selection: { + altText: string | null; + externalUrl: string; + url: string; + classList: string[]; + }; +} + +interface ImageSettingsModalInternalProps extends ImageSettingsModalCommonProps { + imgDimensions: OrigImageDimensions; + setImgDimensions: (dimensions: OrigImageDimensions) => void; +} + +interface ImageSettingsModalProps extends ImageSettingsModalCommonProps { + saveToEditor: (config: HTMLImageAttrs) => void; +} + +export interface ImageSettingsModalFormProps + extends ImageSettingsModalCommonProps, ImageSettingsModalInternalProps +{ + initialValues: T; + validationSchema: Yup.ObjectSchema>; + handleSubmit: (values: T, actions: any) => void; + processValues?: (values: T) => void; +} + +const ImageSettingsModalBase = ({ + isOpen, + close, + returnToSelection, + selection, + imgDimensions, + setImgDimensions, +}: ImageSettingsModalInternalProps) => { + const intl = useIntl(); + const formik = useFormikContext(); + + const setOriginalDimensions = React.useCallback(async (event: React.ChangeEvent) => { + const img = event.currentTarget as HTMLImageElement; + setImgDimensions({ width: img.naturalWidth, height: img.naturalHeight }); + await formik.setFieldValue('width', img.naturalWidth, false); + await formik.setFieldValue('height', img.naturalHeight, false); + }, [formik]); + + const handleDismissError = React.useCallback(() => formik.setFieldError('altText', ''), [formik]); + + return ( + + {intl.formatMessage(messages.saveButtonLabel)} + + } + isOpen={isOpen} + title={intl.formatMessage(messages.titleLabel)} + > + + {intl.formatMessage(messages.altTextError)} + + +
+
+
+ +
+
+
+ + + +
+
+
+ ); +}; + +export const ImageSettingsModalForm = ({ + isOpen, + close, + returnToSelection, + selection, + imgDimensions, + setImgDimensions, + initialValues, + validationSchema, + handleSubmit, + processValues, +}: ImageSettingsModalFormProps) => ( + { + if (processValues) { processValues(values); } + handleSubmit(values, formikHelpers); + }, + [handleSubmit, processValues], + )} + > + + +); + +/** + * Modal display wrapping the dimension and alt-text controls for image tags + * inserted into the TextEditor TinyMCE context. + * Provides a thumbnail and populates dimension and alt-text controls. + * @param {Object} props + * @param {bool} props.isOpen - is the modal open? + * @param {func} props.close - close the modal + * @param {Object} props.selection - current image selection object + * @param {func} props.saveToEditor - save the current settings to the editor + * @param {func} props.returnToSelection - return to image selection + */ +const ImageSettingsModal = ({ + close, + isOpen, + returnToSelection, + saveToEditor, + selection, +}: ImageSettingsModalProps) => { + const intl = useIntl(); + const [imgDimensions, setImgDimensions] = React.useState({ width: 0, height: 0 }); + const initialValues: ImageConfig = { + isLocked: true, + width: imgDimensions.width, + height: imgDimensions.height, + isDecorative: !selection.altText, + altText: selection.altText || '', + classList: selection.classList, + }; + const validationSchema = Yup.object().shape({ + isLocked: Yup.boolean(), + isDecorative: Yup.boolean(), + width: Yup.string().trim().matches(/^[0-9]+%?$/).required(), + height: Yup.string().trim().matches(/^[0-9]+%?$/).required(), + classList: Yup.array().of(Yup.string()).default([]), + altText: Yup.string().when('isDecorative', { + is: true, + otherwise: (schema) => schema.required(intl.formatMessage(messages.altTextLocalFeedback)), + }), + }); + + const handleSubmit = React.useCallback((value: ImageConfig, actions: FormikBag) => { + const { width, height, isDecorative, isLocked, ...imgAttrs } = value; + saveToEditor({ + dimensions: { + width, + height, + }, + ...imgAttrs, + }); + actions.setSubmitting(false); + }, [saveToEditor]); + + return ( + + ); +}; + +export default ImageSettingsModal; diff --git a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/types.ts b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/types.ts new file mode 100644 index 0000000000..d7d60337f6 --- /dev/null +++ b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/types.ts @@ -0,0 +1,25 @@ +export interface ImageDimensions { + width: string | number; + height: string | number; +} + +export interface OrigImageDimensions { + width: number; + height: number; +} + +export interface AltText { + altText: string; + isDecorative: boolean; +} + +export interface ImageConfig extends ImageDimensions, AltText { + isLocked: boolean; + classList?: string[]; +} + +export interface HTMLImageAttrs { + dimensions: ImageDimensions; + altText: string; + classList?: string[]; +} diff --git a/src/editors/sharedComponents/ImageUploadModal/index.jsx b/src/editors/sharedComponents/ImageUploadModal/index.jsx index 3756eeb0bf..32262e5bac 100644 --- a/src/editors/sharedComponents/ImageUploadModal/index.jsx +++ b/src/editors/sharedComponents/ImageUploadModal/index.jsx @@ -31,11 +31,14 @@ export const imgProps = ({ const index = url.indexOf('static/'); url = url.substring(index); } + const { dimensions, altText, isDecorative, classList, ...imgAttrs } = settings; return { src: url, - alt: settings.isDecorative ? '' : settings.altText, - width: settings.dimensions.width, - height: settings.dimensions.height, + alt: isDecorative ? '' : altText, + width: dimensions.width, + height: dimensions.height, + class: (classList ?? []).join(' '), + ...imgAttrs, }; }; @@ -75,7 +78,6 @@ export const updateImagesRef = ({ height, width, }); - // eslint-disable-next-line no-param-reassign images.current = imageAlreadyExists ? mappedImages : [...images.current, newImage]; }; diff --git a/src/editors/sharedComponents/ImageUploadModal/index.test.tsx b/src/editors/sharedComponents/ImageUploadModal/index.test.tsx index f16b1972f4..cfd532fc13 100644 --- a/src/editors/sharedComponents/ImageUploadModal/index.test.tsx +++ b/src/editors/sharedComponents/ImageUploadModal/index.test.tsx @@ -22,6 +22,7 @@ const settings = { width: 2022, height: 1619, }, + classList: [], }; const mockImage = { @@ -56,26 +57,29 @@ describe('ImageUploadModal', () => { alt: settings.altText, width: settings.dimensions.width, height: settings.dimensions.height, + class: '' }; - const testImgTag = (args) => { + const testImgTag = ({expected, settings}) => { const output = hooks.imgTag({ - settings: args.settings, + settings, selection, lmsEndpointUrl: 'sOmE', editorType: 'tinyMCE', isLibrary: true, }); - expect(output).toEqual(``); + expect(output).toEqual(``); }; - test('It returns a html string which matches an image tag', () => { + test.each([ + [ settings, expected ], + [ {...settings, isDecorative: true}, {...expected, alt: ''} ], + [ {...settings, classList: []}, {...expected} ], + [ {...settings, classList: null}, {...expected} ], + [ {...settings, classList: undefined}, {...expected} ], + [ {...settings, classList: ['class1', 'class2']}, {...expected, class: 'class1 class2'} ], + [ {...settings, title: "some title"}, {...expected, title: 'some title'} ], + ])('It returns a html string which matches an image tag', (settings, expected) => { testImgTag({ settings, expected }); }); - test('If isDecorative is true, alt text is an empty string', () => { - testImgTag({ - settings: { ...settings, isDecorative: true }, - expected: { ...expected, alt: '' }, - }); - }); }); describe('createSaveCallback', () => { const updateImageDimensionsSpy = jest.spyOn(tinyMceHooks, 'updateImageDimensions'); diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx index 273d552019..bb8446050b 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -18,13 +18,14 @@ import { import messages from './messages'; import './index.scss'; -import { sortKeys, sortMessages } from '../../containers/VideoGallery/utils'; const SearchSort = ({ searchString, onSearchChange, clearSearchString, sortBy, + sortKeys, + sortMessages, onSortClick, filterBy, onFilterClick, @@ -135,6 +136,8 @@ SearchSort.propTypes = { onFilterClick: PropTypes.func, filterKeys: PropTypes.shape({}), filterMessages: PropTypes.shape({}), + sortKeys: PropTypes.shape({}).isRequired, + sortMessages: PropTypes.shape({}).isRequired, showSwitch: PropTypes.bool, switchMessage: PropTypes.shape({}).isRequired, onSwitchClick: PropTypes.func, diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js index e45b13ddc8..9a99d05d02 100644 --- a/src/editors/sharedComponents/TinyMceWidget/hooks.test.js +++ b/src/editors/sharedComponents/TinyMceWidget/hooks.test.js @@ -456,6 +456,7 @@ describe('TinyMceEditor hooks', () => { altText: mockNode.alt, width: mockImage.width, height: mockImage.height, + classList: [], }); }); @@ -484,6 +485,7 @@ describe('TinyMceEditor hooks', () => { altText: mockNode.alt, width: null, height: null, + classList: [], }); }); }); diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.ts b/src/editors/sharedComponents/TinyMceWidget/hooks.ts index 4816286d1f..17f0583413 100644 --- a/src/editors/sharedComponents/TinyMceWidget/hooks.ts +++ b/src/editors/sharedComponents/TinyMceWidget/hooks.ts @@ -290,6 +290,7 @@ export const openModalWithSelectedImage = ({ altText: tinyMceHTML.alt, width, height, + classList: Array.from(tinyMceHTML.classList ?? []), }); openImgModal(); diff --git a/src/plugin-slots/ImageAdditionalSettingsSlot/README.md b/src/plugin-slots/ImageAdditionalSettingsSlot/README.md new file mode 100644 index 0000000000..7f048f6720 --- /dev/null +++ b/src/plugin-slots/ImageAdditionalSettingsSlot/README.md @@ -0,0 +1,67 @@ +# Image Additional Settings Slot + +### Slot ID: `org.openedx.frontend.authoring.image_additional_settings.v1` + +## Description + +This slot is used to add additional settings to the image settings modal. + +## Example + +The following `env.config.jsx` will add a checkbox that will apply or remove the 'showcase' class to the selected image. + +![Screenshot of the image settings modal with additional settings](./images/image-additional-settings-slot-example.png) + +```jsx +import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { Form } from '@openedx/paragon'; +import { useFormikContext } from 'formik'; +import React from 'react'; + +const ShowcaseClassEditor = () => { + const formik = useFormikContext(); + const { classList } = formik.values; + + const checked = classList.includes('showcase'); + + const handleChange = async (event) => { + // Toggle the 'showcase' class based on the checkbox state + const filteredList = classList.filter(c => c !== 'showcase'); + const newList = event.target.checked + ? [...filteredList, 'showcase'] + : filteredList; + await formik.setFieldValue('classList', newList); + }; + + return ( + + + + Showcase Image + + + ); +}; + +const config = { + pluginSlots: { + 'org.openedx.frontend.authoring.image_additional_settings.v1': { + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'showcase_class_editor', + type: 'DIRECT_PLUGIN', + RenderWidget: ShowcaseClassEditor, + }, + }, + ], + }, + }, +}; + +export default config; +``` diff --git a/src/plugin-slots/ImageAdditionalSettingsSlot/images/image-additional-settings-slot-example.png b/src/plugin-slots/ImageAdditionalSettingsSlot/images/image-additional-settings-slot-example.png new file mode 100644 index 0000000000..d84079ea95 Binary files /dev/null and b/src/plugin-slots/ImageAdditionalSettingsSlot/images/image-additional-settings-slot-example.png differ diff --git a/src/plugin-slots/ImageAdditionalSettingsSlot/index.tsx b/src/plugin-slots/ImageAdditionalSettingsSlot/index.tsx new file mode 100644 index 0000000000..1ac7d0c9d4 --- /dev/null +++ b/src/plugin-slots/ImageAdditionalSettingsSlot/index.tsx @@ -0,0 +1,5 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework/dist'; + +export const ImageAdditionalSettingsSlot = () => ( + +); diff --git a/src/plugin-slots/ImageSettingsModalSlot/README.md b/src/plugin-slots/ImageSettingsModalSlot/README.md new file mode 100644 index 0000000000..c884554950 --- /dev/null +++ b/src/plugin-slots/ImageSettingsModalSlot/README.md @@ -0,0 +1,47 @@ +# Image Settings Modal Slot + +### Slot ID: `org.openedx.frontend.authoring.image_settings_modal.v1` + +### Plugin Props: + +- `initialValues` - Object. The initial values for the Formik form. +- `validationSchema` - Object. The Yup validation schema for the form. +- `processValues` - Function. A function to process form values before submission. + +## Description + +This slot wraps the image settings modal. It can be used to modify the form's initial values, validation schema, or to wrap the entire modal component. You can also use this slot to add validation and initial values to the form. The `processValues` function is called to clean up values being submitted. + +Combined with `ImageAdditionalSettingsSlot`, this slot allows extending the image editing capabilities of the image settings modal. + +Since this slot wraps a modal that uses a React portal, you can't use it to wrap the modal in another div etc. + +## Example + +The following `env.config.jsx` will modify the image settings modal to log the values being submitted. + +```jsx +import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.authoring.image_settings_modal.v1': { + keepDefault: true, + plugins: [ + { + op: PLUGIN_OPERATIONS.Modify, + widgetId: 'default_contents', + fn: (widget) => { + widget.content.processValues = (values) => { + console.log('Processing values:', values); + }; + return widget; + }, + }, + ], + }, + }, +}; + +export default config; +``` diff --git a/src/plugin-slots/ImageSettingsModalSlot/index.tsx b/src/plugin-slots/ImageSettingsModalSlot/index.tsx new file mode 100644 index 0000000000..edf96f38ab --- /dev/null +++ b/src/plugin-slots/ImageSettingsModalSlot/index.tsx @@ -0,0 +1,47 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { + ImageSettingsModalForm, + type ImageSettingsModalFormProps, +} from '@src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal'; + +import React from 'react'; + +const ImageSettingsModalSlot = ({ + isOpen, + close, + returnToSelection, + selection, + imgDimensions, + setImgDimensions, + initialValues, + validationSchema, + handleSubmit, + processValues, +}: ImageSettingsModalFormProps) => ( + + + +); + +export default ImageSettingsModalSlot;