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();