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 3528526 commit 864a07f
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 83 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

49 changes: 41 additions & 8 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,9 +26,22 @@ 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
* canCreate - whether this field can create links or not
* canCreate - whether this field can create new 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, canCreate }) => {
const LinkField = ({
value = null,
onChange,
types = [],
actions,
isMulti = false,
canCreate,
ownerID,
ownerClass,
ownerRelation,
}) => {
const [data, setData] = useState({});
const [editingID, setEditingID] = useState(0);

Expand Down Expand Up @@ -94,7 +110,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 @@ -155,19 +177,27 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
const renderPicker = isMulti || Object.keys(data).length === 0;
const renderModal = Boolean(editingID);

return <>
{ renderPicker && <LinkPicker canCreate={canCreate} onModalSuccess={onModalSuccess} onModalClosed={onModalClosed} types={types} /> }
<div> { renderLinks() } </div>
return <LinkFieldContext.Provider value={{ ownerID, ownerClass, ownerRelation }}>
{ renderPicker && <LinkPicker
onModalSuccess={onModalSuccess}
onModalClosed={onModalClosed}
types={types}
canCreate={canCreate}
/> }
<div> { renderLinks() } </div>
{ renderModal && <LinkModalContainer
types={types}
typeKey={data[editingID]?.typeKey}
isOpen={Boolean(editingID)}
onSuccess={onModalSuccess}
onClosed={onModalClosed}
linkID={editingID}
ownerID={ownerID}
ownerClass={ownerClass}
ownerRelation={ownerRelation}
/>
}
</>;
</LinkFieldContext.Provider>;
};

LinkField.propTypes = {
Expand All @@ -177,6 +207,9 @@ LinkField.propTypes = {
actions: PropTypes.object.isRequired,
isMulti: PropTypes.bool,
canCreate: PropTypes.bool.isRequired,
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 @@ -135,6 +138,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 @@ -215,6 +228,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 @@ -241,6 +264,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 @@ -358,4 +390,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;
}
}
11 changes: 11 additions & 0 deletions src/Form/LinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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;
Expand Down Expand Up @@ -74,13 +77,21 @@ protected function getDefaultAttributes(): array
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->Value();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
$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;
}
}
31 changes: 9 additions & 22 deletions src/Form/MultiLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use LogicException;
use SilverStripe\Forms\FormField;
use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait;
use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Relation;
Expand Down Expand Up @@ -41,33 +42,15 @@ public function setValue($value, $data = null)
return parent::setValue($ids, $data);
}

public function saveInto(DataObjectInterface $record)
{
$fieldName = $this->getName();
if (!$fieldName) {
throw new LogicException('LinkField must have a name');
}

$relation = $record->hasMethod($fieldName) ? $record->$fieldName() : null;
if (!$relation) {
throw new LogicException("{$record->ClassName} is missing the relation '$fieldName'");
}

// Use RelationList rather than Relation here since some Relation classes don't allow setting value - but RelationList does.
if (!($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
throw new LogicException("'$fieldName()' method on {$record->ClassName} doesn't return a relation list");
} else {
$relation->setByIDList($this->getValueArray() ?? []);
}

return $this;
}

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

Expand All @@ -84,6 +67,10 @@ protected function getDefaultAttributes(): array
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->getValueArray();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
$ownerFields = $this->getOwnerFields();
$attributes['data-owner-id'] = $ownerFields['ID'];
$attributes['data-owner-class'] = $ownerFields['Class'];
$attributes['data-owner-relation'] = $ownerFields['Relation'];
return $attributes;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Form/Traits/LinkFieldGetOwnerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,19 @@ private function getOwner(): DataObject
}
return $owner;
}

private function getOwnerFields(): array
{
$owner = $this->getOwner();
$relation = $this->getName();
// Elemental content block
if (preg_match('#^PageElements_[0-9]+_(.+)$#', $relation, $matches)) {
$relation = $matches[1];
}
return [
'ID' => $owner->ID,
'Class' => $owner::class,
'Relation' => $relation,
];
}
}
Loading

0 comments on commit 864a07f

Please sign in to comment.