Skip to content

Commit

Permalink
NEW add multi link support
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Rainville committed Jul 11, 2023
1 parent 469ad24 commit dd4cf68
Show file tree
Hide file tree
Showing 48 changed files with 1,279 additions and 295 deletions.
2 changes: 1 addition & 1 deletion _graphql/queries.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'readLinkDescription(dataStr: String!)':
type: LinkDescription
type: '[LinkDescription]'
resolver: ['SilverStripe\LinkField\GraphQL\LinkDescriptionResolver', 'resolve']
'readLinkTypes(keys: [ID])':
type: '[LinkType]'
Expand Down
3 changes: 3 additions & 0 deletions _graphql/types.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
LinkDescription:
description: Given some Link data, computes the matching description
fields:
id: ID
title: String
description: String

LinkType:
Expand All @@ -9,3 +11,4 @@ LinkType:
key: ID
handlerName: String!
title: String!
icon: String!
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions client/src/boot/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* global document */
/* eslint-disable */
import Config from 'lib/Config';
import registerReducers from './registerReducers';
import registerComponents from './registerComponents';
import registerQueries from './registerQueries';
Expand Down
4 changes: 4 additions & 0 deletions client/src/boot/registerComponents.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable */
import Injector from 'lib/Injector';
import LinkPicker from 'components/LinkPicker/LinkPicker';
import MultiLinkPicker from 'components/MultiLinkPicker/MultiLinkPicker';
import LinkField from 'components/LinkField/LinkField';
import MultiLinkField from 'components/MultiLinkField/MultiLinkField';
import LinkModal from 'components/LinkModal/LinkModal';
import FileLinkModal from 'components/LinkModal/FileLinkModal';

Expand All @@ -10,6 +12,8 @@ const registerComponents = () => {
Injector.component.registerMany({
LinkPicker,
LinkField,
MultiLinkPicker,
MultiLinkField,
'LinkModal.FormBuilderModal': LinkModal,
'LinkModal.InsertMediaModal': FileLinkModal
});
Expand Down
22 changes: 0 additions & 22 deletions client/src/boot/registerReducers.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,4 @@
/* eslint-disable */
import Injector from 'lib/Injector';
import { combineReducers } from 'redux';
// import gallery from 'state/gallery/GalleryReducer';
// import queuedFiles from 'state/queuedFiles/QueuedFilesReducer';
// import uploadField from 'state/uploadField/UploadFieldReducer';
// import previewField from 'state/previewField/PreviewFieldReducer';
// import imageLoad from 'state/imageLoad/ImageLoadReducer';
// import displaySearch from 'state/displaySearch/DisplaySearchReducer';
// import confirmDeletion from 'state/confirmDeletion/ConfirmDeletionReducer';
// import modal from 'state/modal/ModalReducer';

const registerReducers = () => {
// Injector.reducer.register('assetAdmin', combineReducers({
// gallery,
// queuedFiles,
// uploadField,
// previewField,
// imageLoad,
// displaySearch,
// confirmDeletion,
// modal
// }));
};

export default registerReducers;
106 changes: 106 additions & 0 deletions client/src/components/AbstractLinkField/AbstractLinkField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { Fragment, useState } from 'react';
import { loadComponent } from 'lib/Injector';
import PropTypes from 'prop-types';
import LinkType from '../../types/LinkType';
import LinkSummary from '../../types/LinkSummary';

/**
* Underlying implementation of the LinkField. This is used for both the Single LinkField
* and MultiLinkField. It should not be used directly.
*/
const AbstractLinkField = ({
id, loading, Loading, Picker, onChange, types,
clearLinkData, buildLinkProps, updateLinkData, selectLinkData
}) => {
// Render a loading indicator if we're still fetching some data from the server
if (loading) {
return <Loading />;
}

// When editing is true, wu display a modal to let the user edit the link data
const [editingId, setEditingId] = useState(false);
// newTypeKey define what link type we are using for brand new links
const [newTypeKey, setNewTypeKey] = useState('');

const selectedLinkData = selectLinkData(editingId);
const modalType = types[(selectedLinkData && selectedLinkData.typeKey) || newTypeKey];

// When the use clears the link data, we call onchange with an empty object
const onClear = (event, linkId) => {
if (typeof onChange === 'function') {
onChange(event, { id, value: clearLinkData(linkId) });
}
};

const linkProps = {
...buildLinkProps(),
onEdit: (linkId) => { setEditingId(linkId); },
onClear,
onSelect: (key) => {
setNewTypeKey(key);
setEditingId(true);
},
types: Object.values(types)
};

const onModalSubmit = (submittedData) => {
// Remove unneeded keys from submitted data
// eslint-disable-next-line camelcase
const { SecurityID, action_insert, ...newLinkData } = submittedData;
if (typeof onChange === 'function') {
// onChange expect an event object which we don't have
onChange(undefined, { id, value: updateLinkData(newLinkData) });
}
// Close the modal
setEditingId(false);
setNewTypeKey('');
return Promise.resolve();
};

const modalProps = {
type: modalType,
editing: editingId !== false,
onSubmit: onModalSubmit,
onClosed: () => {
setEditingId(false);
return Promise.resolve();
},
data: selectedLinkData
};

// Different link types might have different Link modal
const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal';
const LinkModal = loadComponent(`LinkModal.${handlerName}`);

return (
<Fragment>
<Picker {...linkProps} />
<LinkModal {...modalProps} />
</Fragment>
);
};

/**
* These props are expected to be passthrough from tho parent component.
*/
export const linkFieldPropTypes = {
id: PropTypes.string.isRequired,
loading: PropTypes.bool,
Loading: PropTypes.elementType,
data: PropTypes.any,
Picker: PropTypes.elementType,
onChange: PropTypes.func,
types: PropTypes.objectOf(LinkType),
linkDescriptions: PropTypes.arrayOf(LinkSummary),
};

AbstractLinkField.propTypes = {
...linkFieldPropTypes,
// These props need to be provided by the specific implementation
clearLinkData: PropTypes.func.isRequired,
buildLinkProps: PropTypes.func.isRequired,
updateLinkData: PropTypes.func.isRequired,
selectLinkData: PropTypes.func.isRequired,
};

export default AbstractLinkField;
31 changes: 31 additions & 0 deletions client/src/components/AbstractLinkField/linkFieldHOC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { compose } from 'redux';
import { withApollo } from '@apollo/client/react/hoc';
import { injectGraphql } from 'lib/Injector';
import fieldHolder from 'components/FieldHolder/FieldHolder';

/**
* When getting data from entwine, we might get it in a plain JSON string.
* This method rewrites tho data to a normalise format.
*/
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} />;
});


/**
* Wires a Link field into GraphQL normalise the initial data to a proper objects
*/
const linkFieldHOC = compose(
stringifyData,
injectGraphql('readLinkTypes'),
injectGraphql('readLinkDescription'),
withApollo,
fieldHolder
);

export default linkFieldHOC;
18 changes: 18 additions & 0 deletions client/src/components/LinkBox/LinkBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
* Wraps children in a bok with rounder corners and a form control style.
*/
const LinkBox = ({ className, children }) => (
<div className={classnames('link-box', 'form-control', className)}>
{ children }
</div>
);

LinkBox.propTypes = {
className: PropTypes.string,
};

export default LinkBox;
14 changes: 14 additions & 0 deletions client/src/components/LinkBox/LinkBox.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.link-box {
display: flex;
height: 54px;
background: white;
width: 100%;
align-items: stretch;
cursor: pointer;
padding: 0;
box-shadow: none;

&.font-icon-link::before {
margin: $spacer-xs;
}
}
121 changes: 41 additions & 80 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,51 @@
import React, { Fragment, useState } from 'react';
import React from 'react';
import { compose } from 'redux';
import { inject, injectGraphql, loadComponent } from 'lib/Injector';
import fieldHolder from 'components/FieldHolder/FieldHolder';

const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => {
if (loading) {
return <Loading />;
}

const [editing, setEditing] = useState(false);
const [newTypeKey, setNewTypeKey] = useState('');

const onClear = (event) => {
if (typeof onChange !== 'function') {
return;
}

onChange(event, { id, value: {} });
};

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

let title = data ? data.Title : '';

if (!title) {
title = data ? data.TitleRelField : '';
}

const linkProps = {
title,
link: type ? { type, title, description: linkDescription } : undefined,
onEdit: () => { setEditing(true); },
onClear,
onSelect: (key) => {
setNewTypeKey(key);
setEditing(true);
import { inject } from 'lib/Injector';
import PropTypes from 'prop-types';
import LinkData from '../../types/LinkData';
import AbstractLinkField, { linkFieldPropTypes } from '../AbstractLinkField/AbstractLinkField';
import linkFieldHOC from '../AbstractLinkField/linkFieldHOC';

/**
* Renders a Field allowing the selection of a single link.
*/
const LinkField = (props) => {
const staticProps = {
buildLinkProps: () => {
const { data, linkDescriptions, types } = props;

// Try to read the link type from the link data or use newTypeKey
const { typeKey } = data;
const type = types[typeKey];

// Read link title and description
const linkDescription = linkDescriptions.length > 0 ? linkDescriptions[0] : {};
const { title, description } = linkDescription;
return {
title,
description,
type: type || undefined,
};
},
types: Object.values(types)
clearLinkData: () => ({}),
updateLinkData: newLinkData => newLinkData,
selectLinkData: () => (props.data),
};

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,
editing,
onSubmit: onModalSubmit,
onClosed: () => {
setEditing(false);
},
data
};

const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal';
const LinkModal = loadComponent(`LinkModal.${handlerName}`);
return <AbstractLinkField {...props} {...staticProps} />;
};

return <Fragment>
<LinkPicker {...linkProps} />
<LinkModal {...modalProps} />
</Fragment>;
LinkField.propTypes = {
...linkFieldPropTypes,
data: LinkData
};

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} />;
});
export { LinkField as Component };

export default compose(
inject(['LinkPicker', 'Loading']),
injectGraphql('readLinkTypes'),
stringifyData,
injectGraphql('readLinkDescription'),
fieldHolder
inject(
['LinkPicker', 'Loading'],
(LinkPicker, Loading) => ({ Picker: LinkPicker, Loading })
),
linkFieldHOC
)(LinkField);
Loading

0 comments on commit dd4cf68

Please sign in to comment.