Skip to content

Commit

Permalink
ENH Save relations on link creation
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Dec 21, 2023
1 parent 4e6c662 commit 6e1b488
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 197 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

37 changes: 32 additions & 5 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, createContext } from 'react';
import { bindActionCreators, compose } from 'redux';
import { connect } from 'react-redux';
import { injectGraphql } from 'lib/Injector';
Expand All @@ -13,6 +13,9 @@ import backend from 'lib/Backend';
import Config from 'lib/Config';
import PropTypes from 'prop-types';
import i18n from 'i18n';
import url from 'url';

export const LinkFieldContext = createContext(null);

// section used in window.ss config
const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
Expand All @@ -23,8 +26,20 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
* types - types of the Link passed from LinkField entwine
* actions - object of redux actions
* isMulti - whether this field handles multiple links or not
* ownerID - ID of the owner DataObject
* ownerClass - class name of the owner DataObject
* ownerRelation - name of the relation on the owner DataObject
*/
const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false }) => {
const LinkField = ({
value = null,
onChange,
types = [],
actions,
isMulti = false,
ownerID,
ownerClass,
ownerRelation,
}) => {
const [data, setData] = useState({});
const [editingID, setEditingID] = useState(0);

Expand Down Expand Up @@ -93,7 +108,13 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
* Update the component when the 'Clear' button in the LinkPicker is clicked
*/
const onClear = (linkID) => {
const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`;
let endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`;
const parsedURL = url.parse(endpoint);
const parsedQs = qs.parse(parsedURL.query);
parsedQs.ownerID = ownerID;
parsedQs.ownerClass = ownerClass;
parsedQs.ownerRelation = ownerRelation;
endpoint = url.format({ ...parsedURL, search: qs.stringify(parsedQs)});
// CSRF token 'X-SecurityID' headers needs to be present for destructive requests
backend.delete(endpoint, {}, { 'X-SecurityID': Config.get('SecurityID') })
.then(() => {
Expand Down Expand Up @@ -153,7 +174,7 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
const renderPicker = isMulti || Object.keys(data).length === 0;
const renderModal = Boolean(editingID);

return <>
return <LinkFieldContext.Provider value={{ ownerID, ownerClass, ownerRelation }}>
{ renderPicker && <LinkPicker onModalSuccess={onModalSuccess} onModalClosed={onModalClosed} types={types} /> }
<div> { renderLinks() } </div>
{ renderModal && <LinkModalContainer
Expand All @@ -163,9 +184,12 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
onSuccess={onModalSuccess}
onClosed={onModalClosed}
linkID={editingID}
ownerID={ownerID}
ownerClass={ownerClass}
ownerRelation={ownerRelation}
/>
}
</>;
</LinkFieldContext.Provider>;
};

LinkField.propTypes = {
Expand All @@ -174,6 +198,9 @@ LinkField.propTypes = {
types: PropTypes.objectOf(LinkType).isRequired,
actions: PropTypes.object.isRequired,
isMulti: PropTypes.bool,
ownerID: PropTypes.number.isRequired,
ownerClass: PropTypes.string.isRequired,
ownerRelation: PropTypes.string.isRequired,
};

// redux actions loaded into props - used to get toast notifications
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/LinkModal/LinkModal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable */
import React from 'react';
import React, { useContext } from 'react'
import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal';
import { LinkFieldContext } from 'components/LinkField/LinkField';
import url from 'url';
import qs from 'qs';
import Config from 'lib/Config';
Expand All @@ -11,13 +12,17 @@ const buildSchemaUrl = (typeKey, linkID) => {
const parsedURL = url.parse(schemaUrl);
const parsedQs = qs.parse(parsedURL.query);
parsedQs.typeKey = typeKey;
const { ownerID, ownerClass, ownerRelation } = useContext(LinkFieldContext);
parsedQs.ownerID = ownerID;
parsedQs.ownerClass = ownerClass;
parsedQs.ownerRelation = ownerRelation;
for (const prop of ['href', 'path', 'pathname']) {
parsedURL[prop] = `${parsedURL[prop]}/${linkID}`;
}
return url.format({ ...parsedURL, search: qs.stringify(parsedQs)});
}

const LinkModal = ({ typeTitle, typeKey, linkID = 0, isOpen, onSuccess, onClosed}) => {
const LinkModal = ({ typeTitle, typeKey, linkID = 0, isOpen, onSuccess, onClosed }) => {
if (!typeKey) {
return false;
}
Expand Down
7 changes: 5 additions & 2 deletions client/src/entwine/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ jQuery.entwine('ss', ($) => {
* @returns {Object}
*/
getProps() {
const value = this.getInputField().data('value');
const inputField = this.getInputField();
return {
value,
value: inputField.data('value'),
ownerID: inputField.data('owner-id'),
ownerClass: inputField.data('owner-class'),
ownerRelation: inputField.data('owner-relation'),
onChange: this.handleChange.bind(this),
isMulti: this.data('is-multi') ?? false,
types: this.data('types') ?? [],
Expand Down
86 changes: 86 additions & 0 deletions src/Controllers/LinkFieldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\HiddenField;
use SilverStripe\LinkField\Form\LinkField;
use SilverStripe\LinkField\Services\LinkTypeService;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;

class LinkFieldController extends LeftAndMain
{
Expand All @@ -46,6 +48,7 @@ public function getClientConfig()
$clientConfig['form']['linkForm'] = [
// schema() is defined on LeftAndMain
// schemaUrl will get the $ItemID and ?typeKey dynamically suffixed in LinkModal.js
// as well as ownerID, OwnerClass and OwnerRelation
'schemaUrl' => $this->Link('schema/linkForm'),
'deleteUrl' => $this->Link('delete'),
'dataUrl' => $this->Link('data'),
Expand Down Expand Up @@ -134,6 +137,16 @@ public function linkDelete(): HTTPResponse
}
// delete() will also delete any published version immediately
$link->delete();
// Update owner object if this Link is on a has_one relation on the owner
$owner = $this->ownerFromRequest();
$ownerRelation = $this->ownerRelationFromRequest();
$config = Config::forClass($owner->ClassName);
$hasOne = $config->get('has_one');
if (array_key_exists($ownerRelation, $hasOne) && $owner->canEdit()) {
$owner->$ownerRelation = null;
$owner->write();
}
// Send response
$response = $this->getResponse();
$response->addHeader('Content-type', 'application/json');
$response->setBody(json_encode(['success' => true]));
Expand Down Expand Up @@ -214,6 +227,16 @@ public function save(array $data, Form $form): HTTPResponse
$link->write();
}

// Update owner object if this Link is on a has_one relation on the owner
$owner = $this->ownerFromRequest();
$ownerRelation = $this->ownerRelationFromRequest();
$config = Config::forClass($owner->ClassName);
$hasOne = $config->get('has_one');
if (array_key_exists($ownerRelation, $hasOne) && $owner->canEdit()) {
$owner->$ownerRelation = $link;
$owner->write();
}

// Create a new Form so that it has the correct ID for the DataObject when creating
// a new DataObject, as well as anything else on the DataObject that may have been
// updated in an extension hook. We do this so that the FormSchema state is correct
Expand All @@ -240,6 +263,15 @@ private function createLinkForm(Link $link, string $operation): Form
/** @var Form $form */
$form = $formFactory->getForm($this, $name, ['Record' => $link]);

// Add hidden form fields for OwnerID, OwnerClass and OwnerRelation
if ($operation === 'create') {
$owner = $this->ownerFromRequest();
$form->Fields()->push(HiddenField::create('OwnerID')->setValue($owner->ID));
$form->Fields()->push(HiddenField::create('OwnerClass')->setValue($owner->ClassName));
$ownerRelation = $this->ownerRelationFromRequest();
$form->Fields()->push(HiddenField::create('OwnerRelation')->setValue($ownerRelation));
}

// Set where the form is submitted to
$typeKey = LinkTypeService::create()->keyByClassName($link->ClassName);
$form->setFormAction($this->Link("linkForm/$id?typeKey=$typeKey"));
Expand Down Expand Up @@ -357,4 +389,58 @@ private function typeKeyFromRequest(): string
}
return $typeKey;
}

/**
* Get the owner based on the query string params ownerID, ownerClass, ownerRelation
* OR the POST vars OwnerID, OwnerClass, OwnerRelation
*/
private function ownerFromRequest(): DataObject
{
$request = $this->getRequest();
$ownerID = (int) ($request->getVar('ownerID') ?: $request->postVar('OwnerID'));
if ($ownerID === 0) {
$this->jsonError(404, _t('LinkField.INVALID_OWNER_ID', 'Invalid ownerID'));
}
$ownerClass = $request->getVar('ownerClass') ?: $request->postVar('OwnerClass');
if (!is_a($ownerClass, DataObject::class, true)) {
$this->jsonError(404, _t('LinkField.INVALID_OWNER_CLASS', 'Invalid ownerClass'));
}
$ownerRelation = $this->ownerRelationFromRequest();
$config = Config::forClass($ownerClass);
$hasOne = $config->get('has_one');
$hasMany = $config->get('has_many');
$matchedRelation = false;
foreach ([$hasOne, $hasMany] as $property) {
if (!array_key_exists($ownerRelation, $property)) {
continue;
}
$className = $property[$ownerRelation];
if (is_a($className, Link::class, true)) {
$matchedRelation = true;
break;
}
}
if ($matchedRelation) {
/** @var DataObject $ownerClass */
$owner = $ownerClass::get()->byID($ownerID);
if ($owner) {
return $owner;
}
}
$this->jsonError(404, _t('LinkField.INVALID_OWNER', 'Invalid Owner'));
}

/**
* Get the owner relation based on the query string param ownerRelation
* OR the POST var OwnerRelation
*/
private function ownerRelationFromRequest(): string
{
$request = $this->getRequest();
$ownerRelation = $request->getVar('ownerRelation') ?: $request->postVar('OwnerRelation');
if (!$ownerRelation) {
$this->jsonError(404, _t('LinkField.INVALID_OWNER_RELATION', 'Invalid ownerRelation'));
}
return $ownerRelation;
}
}
42 changes: 13 additions & 29 deletions src/Form/LinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
namespace SilverStripe\LinkField\Form;

use LogicException;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\LinkField\Models\Link;
use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait;
use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait;

/**
* Allows CMS users to edit a Link object.
*/
class LinkField extends FormField
{
use AllowedLinkClassesTrait;
use LinkFieldGetOwnerTrait;

protected $schemaComponent = 'LinkField';

Expand All @@ -31,46 +36,25 @@ public function setValue($value, $data = null)
return parent::setValue($id, $data);
}

/**
* @param DataObject|DataObjectInterface $record - A DataObject such as a Page
* @return $this
*/
public function saveInto(DataObjectInterface $record)
{
// Check required relation details are available
$fieldname = $this->getName();
if (!$fieldname) {
throw new LogicException('LinkField must have a name');
}

$linkID = $this->dataValue();
$dbColumn = $fieldname . 'ID';
$record->$dbColumn = $linkID;

// Store the record as the owner of the link.
// Required for permission checks, etc.
$link = Link::get()->byID($linkID);
if ($link) {
$link->OwnerID = $record->ID;
$link->OwnerClass = $record->ClassName;
$link->OwnerRelation = $fieldname;
$link->write();
}

return $this;
}

protected function getDefaultAttributes(): array
{
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->Value();
$ownerFields = $this->getOwnerFields();
$attributes['data-owner-id'] = $ownerFields['ID'];
$attributes['data-owner-class'] = $ownerFields['Class'];
$attributes['data-owner-relation'] = $ownerFields['Relation'];
return $attributes;
}

public function getSchemaDataDefaults()
{
$data = parent::getSchemaDataDefaults();
$data['types'] = json_decode($this->getTypesProps());
$ownerFields = $this->getOwnerFields();
$data['ownerID'] = $ownerFields['ID'];
$data['ownerClass'] = $ownerFields['Class'];
$data['ownerRelation'] = $ownerFields['Relation'];
return $data;
}
}
Loading

0 comments on commit 6e1b488

Please sign in to comment.