Skip to content

Commit

Permalink
Merge pull request #108 from creative-commoners/pulls/4/form-schema
Browse files Browse the repository at this point in the history
NEW LinkFieldController to handle FormSchema
  • Loading branch information
Maxime Rainville authored Nov 9, 2023
2 parents f0a3b25 + 6035de8 commit 24882c6
Show file tree
Hide file tree
Showing 44 changed files with 1,143 additions and 827 deletions.
1 change: 0 additions & 1 deletion _config.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@

// Avoid creating global variables
call_user_func(function () {

});
19 changes: 1 addition & 18 deletions _config/config.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,6 @@
---
Name: linkfield
---

SilverStripe\Admin\LeftAndMain:
extensions:
- SilverStripe\LinkField\Extensions\LeftAndMain

SilverStripe\Admin\ModalController:
extensions:
- SilverStripe\LinkField\Extensions\ModalController

SilverStripe\Forms\TreeDropdownField:
extensions:
- SilverStripe\LinkField\Extensions\AjaxField

SilverStripe\CMS\Forms\AnchorSelectorField:
extensions:
- SilverStripe\LinkField\Extensions\AjaxField

SilverStripe\LinkField\Form\FormFactory:
extensions:
- SilverStripe\LinkField\Extensions\FormFactoryExtension
- SilverStripe\LinkField\Extensions\LeftAndMainExtension
1 change: 0 additions & 1 deletion _config/types.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
Name: linkfield-types
---

SilverStripe\LinkField\Type\Registry:
types:
cms:
Expand Down
3 changes: 0 additions & 3 deletions _graphql/queries.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
'readLinkDescription(dataStr: String!)':
type: LinkDescription
resolver: ['SilverStripe\LinkField\GraphQL\LinkDescriptionResolver', 'resolve']
'readLinkTypes(keys: [ID])':
type: '[LinkType]'
resolver: ['SilverStripe\LinkField\GraphQL\LinkTypeResolver', 'resolve']
6 changes: 0 additions & 6 deletions _graphql/types.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
LinkDescription:
description: Given some Link data, computes the matching description
fields:
description: String

LinkType:
description: Describe a Type of Link that can be managed by a LinkField
fields:
key: ID
handlerName: String!
title: String!
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions client/src/boot/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
/* global document */
/* eslint-disable */
import Config from 'lib/Config';
import registerReducers from './registerReducers';
import registerComponents from './registerComponents';
import registerQueries from './registerQueries';

document.addEventListener('DOMContentLoaded', () => {
registerComponents();

registerQueries();

registerReducers();
});
2 changes: 1 addition & 1 deletion client/src/boot/registerComponents.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

/* eslint-disable */
import Injector from 'lib/Injector';
import LinkPicker from 'components/LinkPicker/LinkPicker';
import LinkField from 'components/LinkField/LinkField';
import LinkModal from 'components/LinkModal/LinkModal';
import FileLinkModal from 'components/LinkModal/FileLinkModal';


const registerComponents = () => {
Injector.component.registerMany({
LinkPicker,
Expand Down
2 changes: 0 additions & 2 deletions client/src/boot/registerQueries.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-disable */
import Injector from 'lib/Injector';
import readLinkTypes from 'state/linkTypes/readLinkTypes';
import readLinkDescription from 'state/linkDescription/readLinkDescription';

const registerQueries = () => {
Injector.query.register('readLinkTypes', readLinkTypes);
Injector.query.register('readLinkDescription', readLinkDescription);
};
export default registerQueries;
26 changes: 0 additions & 26 deletions client/src/boot/registerReducers.js

This file was deleted.

171 changes: 115 additions & 56 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,149 @@
import React, { Fragment, useState } from 'react';
import { compose } from 'redux';
import { inject, injectGraphql, loadComponent } from 'lib/Injector';
import React, { useState, useEffect } from 'react';
import { bindActionCreators, compose } from 'redux';
import { connect } from 'react-redux';
import { injectGraphql, loadComponent } from 'lib/Injector';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import LinkPicker from 'components/LinkPicker/LinkPicker';
import * as toastsActions from 'state/toasts/ToastsActions';
import backend from 'lib/Backend';
import Config from 'lib/Config';
import PropTypes from 'prop-types';

// section used in window.ss config
const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';

/**
* value - ID of the Link passed from JsonField
* onChange - callback function passed from JsonField - used to update the underlying <input> form field
* types - injected by the GraphQL query
* actions - object of redux actions
*/
const LinkField = ({ value, onChange, types, actions }) => {
const linkID = value;
const [typeKey, setTypeKey] = useState('');
const [data, setData] = useState({});
const [editing, setEditing] = useState(false);

const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => {
if (loading) {
return <Loading />;
}
/**
* Call back used by LinkModal after the form has been submitted and the response has been received
*/
const onModalSubmit = async (modalData, action, submitFn) => {
const formSchema = await submitFn();

// slightly annoyingly, on validation error formSchema at this point will not have an errors node
// instead it will have the original formSchema id used for the GET request to get the formSchema i.e.
// admin/linkfield/schema/linkfield/<ItemID>
// instead of the one used by the POST submission i.e.
// admin/linkfield/linkForm/<LinkID>
const hasValidationErrors = formSchema.id.match(/\/schema\/linkfield\/([0-9]+)/);
if (!hasValidationErrors) {
// get link id from formSchema response
const match = formSchema.id.match(/\/linkForm\/([0-9]+)/);
const valueFromSchemaResponse = parseInt(match[1], 10);

// update component state
setEditing(false);

const [editing, setEditing] = useState(false);
const [newTypeKey, setNewTypeKey] = useState('');
// update parent JsonField data id - this is required to update the underlying <input> form field
// so that the Page (or other parent DataObject) gets the Link relation ID set
onChange(valueFromSchemaResponse);

const onClear = (event) => {
if (typeof onChange !== 'function') {
return;
// success toast
actions.toasts.success('Saved link');
}

onChange(event, { id, value: {} });
return Promise.resolve();
};

const { typeKey } = data;
const type = types[typeKey];
const modalType = newTypeKey ? types[newTypeKey] : type;

let title = data ? data.Title : '';
/**
* Call back used by LinkPicker when the 'Clear' button is clicked
*/
const onClear = () => {
const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`;
// CSRF token 'X-SecurityID' headers needs to be present for destructive requests
backend.delete(endpoint, {}, { 'X-SecurityID': Config.get('SecurityID') })
.then(() => {
actions.toasts.success('Deleted link');
})
.catch(() => {
actions.toasts.error('Failed to delete link');
});

// update component state
setTypeKey('');
setData({});

// update parent JsonField data ID used to update the underlying <input> form field
onChange(0);
};

if (!title) {
title = data ? data.TitleRelField : '';
}
const title = data.Title || '';
const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {};
const modalType = typeKey ? types[typeKey] : type;
const handlerName = modalType && modalType.hasOwnProperty('handlerName')
? modalType.handlerName
: 'FormBuilderModal';
const LinkModal = loadComponent(`LinkModal.${handlerName}`);

const linkProps = {
const pickerProps = {
title,
link: type ? { type, title, description: linkDescription } : undefined,
onEdit: () => { setEditing(true); },
description: data.description,
typeTitle: type.title || '',
onEdit: () => {
setEditing(true);
},
onClear,
onSelect: (key) => {
setNewTypeKey(key);
setTypeKey(key);
setEditing(true);
},
types: Object.values(types)
};

const onModalSubmit = (modalData, action, submitFn) => {
const { SecurityID, action_insert: actionInsert, ...value } = modalData;

if (typeof onChange === 'function') {
onChange(event, { id, value });
}

setEditing(false);
setNewTypeKey('');

return Promise.resolve();
};

const modalProps = {
type: modalType,
typeTitle: type.title || '',
typeKey,
editing,
onSubmit: onModalSubmit,
onClosed: () => {
setEditing(false);
},
data
linkID
};

const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal';
const LinkModal = loadComponent(`LinkModal.${handlerName}`);
// read data from endpoint and update component state
useEffect(() => {
if (!editing && linkID) {
const endpoint = `${Config.getSection(section).form.linkForm.dataUrl}/${linkID}`;
backend.get(endpoint)
.then(response => response.json())
.then(responseJson => {
setData(responseJson);
setTypeKey(responseJson.typeKey);
});
}
}, [editing, linkID]);

return <>
<LinkPicker {...pickerProps} />
<LinkModal {...modalProps} />
</>;
};

return <Fragment>
<LinkPicker {...linkProps} />
<LinkModal {...modalProps} />
</Fragment>;
LinkField.propTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};

const stringifyData = (Component) => (({ data, value, ...props }) => {
let dataValue = value || data;
if (typeof dataValue === 'string') {
dataValue = JSON.parse(dataValue);
}
return <Component dataStr={JSON.stringify(dataValue)} {...props} data={dataValue} />;
// redux actions loaded into props - used to get toast notifications
const mapDispatchToProps = (dispatch) => ({
actions: {
toasts: bindActionCreators(toastsActions, dispatch),
},
});

export default compose(
inject(['LinkPicker', 'Loading']),
injectGraphql('readLinkTypes'),
stringifyData,
injectGraphql('readLinkDescription'),
fieldHolder
fieldHolder,
connect(null, mapDispatchToProps)
)(LinkField);
10 changes: 10 additions & 0 deletions client/src/components/LinkModal/FileLinkModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import i18n from 'i18n';
import React, {useEffect} from 'react';
import InsertMediaModal from 'containers/InsertMediaModal/InsertMediaModal';
import {connect} from "react-redux";
import LinkType from 'types/LinkType';
import PropTypes from 'prop-types';

const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => {

Expand Down Expand Up @@ -46,6 +48,14 @@ const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => {
/>;
}

FileLinkModal.propTypes = {
type: LinkType.isRequired,
editing: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
};

function mapStateToProps() {
return {};
}
Expand Down
Loading

0 comments on commit 24882c6

Please sign in to comment.