From 26d21943825117aeb8792d4c5d21e644ab58e844 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Wed, 5 Mar 2025 11:10:46 +1300 Subject: [PATCH] ENH Add campaign-admin support back in --- _config/config.yml | 8 ++ .../src/containers/AssetAdmin/AssetAdmin.js | 1 + .../AssetAdmin/tests/AssetAdmin-test.js | 3 + client/src/containers/Editor/Editor.js | 36 +++++++- .../containers/Editor/tests/Editor-test.js | 36 +++++--- code/Controller/AssetAdmin.php | 83 +++++++++++++++++++ code/Extensions/CampaignAdminExtension.php | 44 ++++++++++ tests/php/Forms/FileFormBuilderTest.php | 48 ++++++++++- .../php/Forms/FolderCreateFormFactoryTest.php | 3 + 9 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 code/Extensions/CampaignAdminExtension.php diff --git a/_config/config.yml b/_config/config.yml index 451eb4088..6277c7269 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -48,6 +48,14 @@ SilverStripe\Core\Injector\Injector: SilverStripe\Forms\FileHandleField: class: SilverStripe\AssetAdmin\Forms\UploadField --- +Name: assetadmincampaigns +Only: + moduleexists: 'silverstripe/campaign-admin' +--- +SilverStripe\AssetAdmin\Forms\FileFormFactory: + extensions: + - 'SilverStripe\AssetAdmin\Extensions\CampaignAdminExtension' +--- Name: assetadminmodals --- SilverStripe\Admin\ModalController: diff --git a/client/src/containers/AssetAdmin/AssetAdmin.js b/client/src/containers/AssetAdmin/AssetAdmin.js index 991d3325d..f57b4d37a 100644 --- a/client/src/containers/AssetAdmin/AssetAdmin.js +++ b/client/src/containers/AssetAdmin/AssetAdmin.js @@ -742,6 +742,7 @@ class AssetAdmin extends Component { onClose: this.handleCloseFile, onSubmit: this.handleSubmitEditor, onUnpublish: this.handleUnpublish, + addToCampaignSchemaUrl: config.form.addToCampaignForm.schemaUrl }; return ; diff --git a/client/src/containers/AssetAdmin/tests/AssetAdmin-test.js b/client/src/containers/AssetAdmin/tests/AssetAdmin-test.js index 7e57ea98e..d63d9d6f6 100644 --- a/client/src/containers/AssetAdmin/tests/AssetAdmin-test.js +++ b/client/src/containers/AssetAdmin/tests/AssetAdmin-test.js @@ -143,6 +143,9 @@ function makeProps(obj = {}) { schemaUrl: '', }, }, + addToCampaignForm: { + schemaUrl: '', + } }, fileId: null, folderId: null, diff --git a/client/src/containers/Editor/Editor.js b/client/src/containers/Editor/Editor.js index 7ee3f7d79..dd2106184 100644 --- a/client/src/containers/Editor/Editor.js +++ b/client/src/containers/Editor/Editor.js @@ -5,6 +5,7 @@ import { bindActionCreators, compose } from 'redux'; import React, { Component } from 'react'; import CONSTANTS from 'constants/index'; import FormBuilderLoader from 'containers/FormBuilderLoader/FormBuilderLoader'; +import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal'; import * as UnsavedFormsActions from 'state/unsavedForms/UnsavedFormsActions'; import PropTypes from 'prop-types'; import { inject } from 'lib/Injector'; @@ -31,10 +32,13 @@ class Editor extends Component { this.handleLoadingSuccess = this.handleLoadingSuccess.bind(this); this.handleLoadingError = this.handleLoadingError.bind(this); this.handleFetchingSchema = this.handleFetchingSchema.bind(this); + this.closeModal = this.closeModal.bind(this); + this.openModal = this.openModal.bind(this); this.createFn = this.createFn.bind(this); this.editorHeader = this.editorHeader.bind(this); this.state = { + openModal: false, loadingForm: false, loadingError: null, file: null, @@ -87,6 +91,12 @@ class Editor extends Component { handleAction(event) { const file = this.state.file; switch (event.currentTarget.name) { + // intercept the Add to Campaign submit and open the modal dialog instead + case 'action_addtocampaign': + this.openModal(); + event.preventDefault(); + + break; case 'action_replacefile': this.replaceFile(); event.preventDefault(); @@ -152,6 +162,7 @@ class Editor extends Component { } else { // If we're already at the top of the form stack, close the editor form onClose(); + this.closeModal(); } if (event) { @@ -159,6 +170,14 @@ class Editor extends Component { } } + openModal() { + this.setState({ openModal: true }); + } + + closeModal() { + this.setState({ openModal: false }); + } + replaceFile() { const hiddenFileInput = document.querySelector('.dz-input-PreviewImage'); @@ -270,8 +289,9 @@ class Editor extends Component { if (!this.state.file) { return null; } - const { FormBuilderLoaderComponent } = this.props; + const { FormBuilderLoaderComponent, FormBuilderModalComponent } = this.props; const formSchemaUrl = this.getFormSchemaUrl(); + const modalSchemaUrl = `${this.props.addToCampaignSchemaUrl}/${this.props.fileId}`; const editorClasses = classnames( 'panel', 'form--no-dividers', 'editor', { 'editor--asset-dropzone--disable': !this.props.enableDropzone @@ -291,6 +311,7 @@ class Editor extends Component {
{message}
); } + const campaignTitle = i18n._t('Admin.ADD_TO_CAMPAIGN', 'Add to campaign'); const Loading = this.props.loadingComponent; return (
@@ -307,6 +328,16 @@ class Editor extends Component { file={this.state.file} /> {error} + { this.state.loadingForm && }
); @@ -325,16 +356,19 @@ Editor.propTypes = { name: PropTypes.string, value: PropTypes.any, })), + addToCampaignSchemaUrl: PropTypes.string, actions: PropTypes.object, showingSubForm: PropTypes.bool, nextType: PropTypes.string, EditorHeaderComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), FormBuilderLoaderComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + FormBuilderModalComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), }; Editor.defaultProps = { EditorHeaderComponent: EditorHeader, FormBuilderLoaderComponent: FormBuilderLoader, + FormBuilderModalComponent: FormBuilderModal, }; function mapDispatchToProps(dispatch) { diff --git a/client/src/containers/Editor/tests/Editor-test.js b/client/src/containers/Editor/tests/Editor-test.js index d747686ff..4a5ad9df9 100644 --- a/client/src/containers/Editor/tests/Editor-test.js +++ b/client/src/containers/Editor/tests/Editor-test.js @@ -78,12 +78,20 @@ function makeProps(obj = {}) { FormBuilderLoaderComponent: ({ createFn, onAction, schemaUrl }) => (
onAction(...nextParams)} data-schema-url={schemaUrl}>{createFn(...createFnParams)}
), + FormBuilderModalComponent: ({ isOpen }) =>
, ...obj }; } -async function awaitLoader() { - await screen.findByTestId('test-form-builder-loader'); +async function openModal() { + const loader = await screen.findByTestId('test-form-builder-loader'); + nextParams = [{ + preventDefault: () => null, + currentTarget: { + name: 'action_addtocampaign' + } + }]; + fireEvent.click(loader); nextParams = [{ preventDefault: () => null, currentTarget: { @@ -109,12 +117,16 @@ test('Editor handleClose Closing editor', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); + let modal = await screen.findByTestId('test-form-builder-modal'); + expect(modal.getAttribute('data-is-open')).toBe('true'); const header = await screen.findByTestId('test-editor-header'); nextAction = 'cancel'; fireEvent.click(header); expect(popFormStackEntry).not.toHaveBeenCalled(); expect(onClose).toHaveBeenCalled(); + modal = await screen.findByTestId('test-form-builder-modal'); + expect(modal.getAttribute('data-is-open')).toBe('false'); expect(header.getAttribute('data-show-button')).toBe(buttonStates.SWITCH); }); @@ -135,12 +147,16 @@ test('Editor handleClose Closing sub form', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); + let modal = await screen.findByTestId('test-form-builder-modal'); + expect(modal.getAttribute('data-is-open')).toBe('true'); const header = await screen.findByTestId('test-editor-header'); nextAction = 'cancel'; fireEvent.click(header); expect(popFormStackEntry).toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); + modal = await screen.findByTestId('test-form-builder-modal'); + expect(modal.getAttribute('data-is-open')).toBe('true'); expect(header.getAttribute('data-show-button')).toBe(buttonStates.SWITCH); }); @@ -173,7 +189,7 @@ test('Editor editorHeader Top Form with detail in dialog', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); const header = await screen.findByTestId('test-editor-header'); nextAction = 'details'; fireEvent.click(header); @@ -192,7 +208,7 @@ test('Editor editorHeader Sub form in dialog', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); const header = await screen.findByTestId('test-editor-header'); expect(header.getAttribute('data-show-button')).toBe(buttonStates.ALWAYS_BACK); }); @@ -212,7 +228,7 @@ test('Editor editorHeader Form for folder', async () => { type: 'folder', }) }); - awaitLoader(); + openModal(); const header = await screen.findByTestId('test-editor-header'); expect(header.getAttribute('data-show-button')).toBe(buttonStates.SWITCH); }); @@ -226,7 +242,7 @@ test('Editor getFormSchemaUrl Plain URL', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); const loader = await screen.findByTestId('test-form-builder-loader'); expect(loader.getAttribute('data-schema-url')).toBe('edit/file/123'); }); @@ -240,7 +256,7 @@ test('Editor getFormSchemaUrl Plain URL', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); const loader = await screen.findByTestId('test-form-builder-loader'); expect(loader.getAttribute('data-schema-url')).toBe('edit/file/123?q=search'); }); @@ -257,7 +273,7 @@ test('Editor getFormSchemaUrl Plain URL', async () => { /> ); resolveBackendGet(makeReadFileResponse()); - awaitLoader(); + openModal(); const loader = await screen.findByTestId('test-form-builder-loader'); expect(loader.getAttribute('data-schema-url')).toBe('edit/file/123?q=search&foo=bar'); }); diff --git a/code/Controller/AssetAdmin.php b/code/Controller/AssetAdmin.php index 441170ece..26b302773 100644 --- a/code/Controller/AssetAdmin.php +++ b/code/Controller/AssetAdmin.php @@ -24,6 +24,7 @@ use SilverStripe\Assets\Image; use SilverStripe\Assets\Storage\AssetNameGenerator; use SilverStripe\Assets\Upload; +use SilverStripe\CampaignAdmin\AddToCampaignHandler; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; @@ -47,6 +48,7 @@ use SilverStripe\ORM\DataList; use SilverStripe\Forms\DateField; use SilverStripe\ORM\DataQuery; +use RuntimeException; /** * AssetAdmin is the 'file store' section of the CMS. @@ -96,6 +98,7 @@ class AssetAdmin extends AssetAdminOpen implements PermissionProvider 'folderCreateForm', 'fileEditForm', 'fileHistoryForm', + 'addToCampaignForm', 'fileInsertForm', 'fileEditorLinkForm', 'schema', @@ -290,6 +293,9 @@ public function getClientConfig(): array 'fileSelectForm' => [ 'schemaUrl' => $this->Link('schema/fileSelectForm') ], + 'addToCampaignForm' => [ + 'schemaUrl' => $this->Link('schema/addToCampaignForm') + ], 'fileHistoryForm' => [ 'schemaUrl' => $this->Link('schema/fileHistoryForm') ], @@ -1423,6 +1429,83 @@ public function generateThumbnails(File $file, $thumbnailLinks = false) return $links; } + /** + * Action handler for adding pages to a campaign + */ + public function addtocampaign(array $data, Form $form): HTTPResponse + { + if (!class_exists(AddToCampaignHandler::class)) { + throw new RuntimeException('AddToCampaignHandler not available'); + } + $id = $data['ID']; + $record = File::get()->byID($id); + + $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm'); + $response = $handler->addToCampaign($record, $data); + $message = $response->getBody(); + if (empty($message)) { + return $response; + } + + // Send extra "message" data with schema response + $extraData = ['message' => $message]; + $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id); + return $this->getSchemaResponse($schemaId, $form, null, $extraData); + } + + /** + * Url handler for add to campaign form + * + * @param HTTPRequest $request + * @return Form + */ + public function addToCampaignForm($request) + { + // Get ID either from posted back value, or url parameter + $id = $request->param('ID') ?: $request->postVar('ID'); + return $this->getAddToCampaignForm($id); + } + + /** + * @param int $id + * @return Form|HTTPResponse + */ + public function getAddToCampaignForm($id) + { + if (!class_exists(AddToCampaignHandler::class)) { + throw new RuntimeException('AddToCampaignHandler not available'); + } + // Get record-specific fields + $record = File::get()->byID($id); + + if (!$record) { + $this->jsonError(404, _t( + __CLASS__.'.ErrorNotFound', + "That {Type} couldn't be found", + ['Type' => File::singleton()->i18n_singular_name()] + )); + return null; + } + if (!$record->canView()) { + $this->jsonError(403, _t( + __CLASS__.'.ErrorItemPermissionDenied', + "You don't have the necessary permissions to modify {ObjectTitle}", + ['ObjectTitle' => $record->i18n_singular_name()] + )); + return null; + } + + $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm'); + $form = $handler->Form($record); + + $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) { + $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id); + return $this->getSchemaResponse($schemaId, $form, $errors); + }); + + return $form; + } + /** * @return Upload */ diff --git a/code/Extensions/CampaignAdminExtension.php b/code/Extensions/CampaignAdminExtension.php new file mode 100644 index 000000000..0306146bc --- /dev/null +++ b/code/Extensions/CampaignAdminExtension.php @@ -0,0 +1,44 @@ + + */ +class CampaignAdminExtension extends Extension +{ + public function __construct() + { + parent::__construct(); + } + + /** + * Update the Popover menu of `FileFormFactory` with the "Add to campaign" button. + * + * @param array $actions + * @param File $record + */ + protected function updatePopoverActions(&$actions, $record) + { + if (!Permission::check('CMS_ACCESS_CampaignAdmin')) { + return; + } + + if ($record && $record->canPublish()) { + $action = FormAction::create( + 'addtocampaign', + _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ADDTOCAMPAIGN', 'Add to campaign') + )->setIcon('page-multiple'); + array_unshift($actions, $action); + } + } +} diff --git a/tests/php/Forms/FileFormBuilderTest.php b/tests/php/Forms/FileFormBuilderTest.php index e375b578f..951e6ecad 100644 --- a/tests/php/Forms/FileFormBuilderTest.php +++ b/tests/php/Forms/FileFormBuilderTest.php @@ -14,6 +14,7 @@ use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\LiteralField; +use SilverStripe\CampaignAdmin\AddToCampaignHandler; class FileFormBuilderTest extends SapphireTest { @@ -51,6 +52,9 @@ protected function tearDown(): void public function testEditFileForm() { + // Ensure campaign-admin extension is not applied! + Config::modify()->remove(FileFormFactory::class, 'extensions'); + $this->logInWithPermission('ADMIN'); $file = $this->objFromFixture(File::class, 'file1'); @@ -91,6 +95,24 @@ public function testEditFileForm() $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); + + if (class_exists(AddToCampaignHandler::class)) { + // Add to campaign should not be there by default + $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + + // Add extension for campaign-admin + Config::modify()->merge( + FileFormFactory::class, + 'extensions', + [CampaignAdminExtension::class] + ); + + $builder = new FileFormFactory(); + $form = $builder->getForm($controller, 'EditForm', ['Record' => $file, 'RequireLinkText' => false]); + + // Add to campaign should now be available + $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } } public function testEditFileFormWithPermissions() @@ -98,7 +120,12 @@ public function testEditFileFormWithPermissions() // Add extension to simulate different permissions File::add_extension(FileExtension::class); - $this->logInWithPermission('CMS_ACCESS_AssetAdmin'); + if (class_exists(CampaignAdminExtension::class)) { + FileFormFactory::add_extension(CampaignAdminExtension::class); + $this->logInWithPermission('CMS_ACCESS_CampaignAdmin'); + } else { + $this->logInWithPermission('CMS_ACCESS_AssetAdmin'); + } /** @var File $file */ $file = $this->objFromFixture(File::class, 'file1'); @@ -112,7 +139,9 @@ public function testEditFileFormWithPermissions() $this->assertNull($form->Actions()->fieldByName('PopoverActions')); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); - + if (class_exiss(CampaignAdminExtension::class)) { + $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); FileExtension::$canDelete = false; @@ -121,6 +150,9 @@ public function testEditFileFormWithPermissions() $form = $builder->getForm($controller, 'EditForm', ['Record' => $file, 'RequireLinkText' => false]); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); + if (class_exiss(CampaignAdminExtension::class)) { + $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); FileExtension::$canDelete = true; @@ -129,6 +161,9 @@ public function testEditFileFormWithPermissions() $form = $builder->getForm($controller, 'EditForm', ['Record' => $file]); $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); + if (class_exiss(CampaignAdminExtension::class)) { + $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); FileExtension::$canDelete = false; @@ -137,6 +172,9 @@ public function testEditFileFormWithPermissions() $form = $builder->getForm($controller, 'EditForm', ['Record' => $file]); $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); + if (class_exiss(CampaignAdminExtension::class)) { + $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } $this->assertNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); FileExtension::$canDelete = true; @@ -147,8 +185,14 @@ public function testEditFileFormWithPermissions() $form = $builder->getForm($controller, 'EditForm', ['Record' => $file, 'RequireLinkText' => false]); $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_delete')); $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_replacefile')); + if (class_exiss(CampaignAdminExtension::class)) { + $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_addtocampaign')); + } $this->assertNotNull($form->Actions()->fieldByName('PopoverActions.action_unpublish')); + if (class_exiss(CampaignAdminExtension::class)) { + FileFormFactory::remove_extension(CampaignAdminExtension::class); + } File::remove_extension(FileExtension::class); } diff --git a/tests/php/Forms/FolderCreateFormFactoryTest.php b/tests/php/Forms/FolderCreateFormFactoryTest.php index fb8817b7c..98e7be106 100644 --- a/tests/php/Forms/FolderCreateFormFactoryTest.php +++ b/tests/php/Forms/FolderCreateFormFactoryTest.php @@ -13,6 +13,9 @@ class FolderCreateFormFactoryTest extends SapphireTest { public function testEditFileForm() { + // Ensure campaign-admin extension is not applied! + Config::modify()->remove(FileFormFactory::class, 'extensions'); + $this->logInWithPermission('ADMIN'); $controller = new AssetAdmin();