Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH Better versioned history #209

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 1 addition & 1 deletion client/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
"LinkField.LINK_MODIFIED_TITLE": "Link has unpublished changes",
"LinkField.LINK_MODIFIED_LABEL": "Modified",
"LinkField.CANNOT_CREATE_LINK": "Cannot create link",
"LinkField.FAILED_TO_LOAD_LINKS": "Failed to load links",
"LinkField.FAILED_TO_LOAD_LINKS": "Failed to load link(s)",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor improvement to the message as it's used for both LinkField and for MultiLinkField

"LinkField.FAILED_TO_SAVE_LINK": "Failed to save link",
"LinkField.SAVE_RECORD_FIRST": "Cannot add links until the record has been saved",
"LinkField.SORT_SUCCESS": "Updated link sort order",
Expand Down
2 changes: 1 addition & 1 deletion client/lang/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"LinkField.LINK_MODIFIED_TITLE": "Link has unpublished changes",
"LinkField.LINK_MODIFIED_LABEL": "Modified",
"LinkField.CANNOT_CREATE_LINK": "Cannot create link",
"LinkField.FAILED_TO_LOAD_LINKS": "Failed to load links",
"LinkField.FAILED_TO_LOAD_LINKS": "Failed to load link(s)",
"LinkField.FAILED_TO_SAVE_LINK": "Failed to save link",
"LinkField.SAVE_RECORD_FIRST": "Cannot add links until the record has been saved",
"LinkField.SORT_SUCCESS": "Updated link sort order",
Expand Down
37 changes: 26 additions & 11 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
* ownerID - ID of the owner DataObject
* ownerClass - class name of the owner DataObject
* ownerRelation - name of the relation on the owner DataObject
* inHistoryViewer - if the field is being viewed in the context of the history viewer
*/
const LinkField = ({
value = null,
onChange,
onChange = () => {},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a default value that's being set now that it's no longer a required prop

types = {},
actions,
isMulti = false,
Expand All @@ -59,6 +60,7 @@ const LinkField = ({
ownerClass,
ownerRelation,
excludeLinkTextField = false,
inHistoryViewer,
}) => {
const [data, setData] = useState({});
const [editingID, setEditingID] = useState(0);
Expand All @@ -67,6 +69,7 @@ const LinkField = ({
const [focusOnNewLinkWhenClosed, setFocusOnNewLinkWhenClosed] = useState(false);
const [focusOnNewLink, setFocusOnNewLink] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingError, setLoadingError] = useState(false);
const [forceFetch, setForceFetch] = useState(0);
const [isSorting, setIsSorting] = useState(false);
const [linksClassName, setLinksClassName] = useState(classnames({'link-picker-links': true}));
Expand Down Expand Up @@ -156,16 +159,16 @@ const LinkField = ({
.then(response => response.json())
.then(responseJson => {
setData(responseJson);
})
.catch(() => {
setLoadingError(true);
})
.finally(() => {
setLoading(false);
// isSorting is set to true on drag start and only set to false here to prevent
// the loading indicator for flickering
setIsSorting(false);
})
.catch(() => {
actions.toasts.error(i18n._t('LinkField.FAILED_TO_LOAD_LINKS', 'Failed to load links'))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to not show a red toast here because this will pop up when viewing old versions of an owner where the link has been deleted. I've changed this to show an inline message when the link fails to load.

setLoading(false);
setIsSorting(false);
});
}
}, [editingID, value && value.length, forceFetch]);

Expand Down Expand Up @@ -441,14 +444,25 @@ const LinkField = ({
});
}

const saveRecordFirst = ownerID === 0;
const renderPicker = !saveRecordFirst && (isMulti || Object.keys(data).length === 0);
const renderModal = !saveRecordFirst && Boolean(editingID);
const saveRecordFirst = !loadingError && ownerID === 0;
const renderLoadingError = loadingError;
const renderPicker = !loadingError && !inHistoryViewer && !saveRecordFirst && (isMulti || Object.keys(data).length === 0);
const renderModal = !loadingError && !saveRecordFirst && Boolean(editingID);
const loadingErrorText = i18n._t('LinkField.FAILED_TO_LOAD_LINKS', 'Failed to load link(s)');
const saveRecordFirstText = i18n._t('LinkField.SAVE_RECORD_FIRST', 'Cannot add links until the record has been saved');
const links = renderLinks();

return <LinkFieldContext.Provider value={{ ownerID, ownerClass, ownerRelation, actions, loading, excludeLinkTextField }}>
return <LinkFieldContext.Provider value={{
ownerID,
ownerClass,
ownerRelation,
actions,
loading,
excludeLinkTextField,
inHistoryViewer
}}>
<div className="link-field__container">
{ renderLoadingError && <div className="link-field__loading-error">{loadingErrorText}</div> }
{ saveRecordFirst && <div className="link-field__save-record-first">{saveRecordFirstText}</div>}
{ loading && !isSorting && !saveRecordFirst && <Loading containerClass="link-field__loading"/> }
{ renderPicker && <LinkPicker
Expand Down Expand Up @@ -478,7 +492,7 @@ const LinkField = ({

LinkField.propTypes = {
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]),
onChange: PropTypes.func.isRequired,
onChange: PropTypes.func,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never have been a required field as it only makes sense in an entwine context

types: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
isMulti: PropTypes.bool,
Expand All @@ -489,6 +503,7 @@ LinkField.propTypes = {
ownerClass: PropTypes.string.isRequired,
ownerRelation: PropTypes.string.isRequired,
excludeLinkTextField: PropTypes.bool,
inHistoryViewer: PropTypes.bool,
};

// redux actions loaded into props - used to get toast notifications
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/LinkField/LinkField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
}
}

.link-field__save-record-first {
.link-field__save-record-first,
.link-field__loading_error {
padding-top: 7px;
}

Expand Down
32 changes: 20 additions & 12 deletions client/src/components/LinkField/tests/LinkField-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global jest, test, expect, document */
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { Component as LinkField } from '../LinkField';
Expand Down Expand Up @@ -50,6 +50,7 @@ function makeProps(obj = {}) {
canCreate: true,
readonly: false,
disabled: false,
inHistoryViewer: false,
ownerID: 123,
ownerClass: 'Page',
ownerRelation: 'MyRelation',
Expand Down Expand Up @@ -87,7 +88,7 @@ test('LinkField returns list of links if they exist', async () => {
test('LinkField will render disabled state if disabled is true', async () => {
const { container } = render(<LinkField {...makeProps({
ownerID: 1,
disabled: true
disabled: true,
})}
/>);
doResolve();
Expand All @@ -99,10 +100,10 @@ test('LinkField will render disabled state if disabled is true', async () => {
test('LinkField will render readonly state if readonly is true', async () => {
const { container } = render(<LinkField {...makeProps({
ownerID: 1,
readonly: true
readonly: true,
value: null,
})}
/>);
doResolve();
await screen.findByText('Cannot create link');
expect(container.querySelectorAll('.link-picker')).toHaveLength(1);
expect(container.querySelectorAll('.link-picker')[0]).toHaveTextContent('Cannot create link');
Expand Down Expand Up @@ -180,15 +181,22 @@ test('LinkField will render loading indicator if ownerID is not 0', async () =>
expect(container.querySelectorAll('.link-picker')).toHaveLength(1);
});

test('LinkField will render link-picker if ownerID is not 0 and has finished loading', async () => {
test('LinkField will render link-picker if ownerID is not 0 and isMulti and has finished loading', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This jest test was technically wrong before - it shouldn't render a .link-picker element if it's non-isMulti and ownerID is not 0 - it should only render that if it's isMulti

const { container } = render(<LinkField {...makeProps({
ownerID: 1
ownerID: 1,
isMulti: true,
})}
/>);
doResolve();
await waitFor(() => {
expect(container.querySelectorAll('.link-field__save-record-first')).toHaveLength(0);
expect(container.querySelectorAll('.link-field__loading')).toHaveLength(0);
expect(container.querySelectorAll('.link-picker')).toHaveLength(1);
}, { timeout: 100 });
await doResolve({ json: () => ({
123: {
Title: 'First title',
Sort: 1,
typeKey: 'mylink',
},
}) });
await screen.findByText('First title');

expect(container.querySelectorAll('.link-field__save-record-first')).toHaveLength(0);
expect(container.querySelectorAll('.link-field__loading')).toHaveLength(0);
expect(container.querySelectorAll('.link-picker')).toHaveLength(1);
});
5 changes: 4 additions & 1 deletion client/src/components/LinkModal/LinkModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ const buildSchemaUrl = (typeKey, linkID) => {
const parsedURL = url.parse(schemaUrl);
const parsedQs = qs.parse(parsedURL.query);
parsedQs.typeKey = typeKey;
const { ownerID, ownerClass, ownerRelation, excludeLinkTextField } = useContext(LinkFieldContext);
const { ownerID, ownerClass, ownerRelation, excludeLinkTextField, inHistoryViewer } = useContext(LinkFieldContext);
parsedQs.ownerID = ownerID;
parsedQs.ownerClass = ownerClass;
parsedQs.ownerRelation = ownerRelation;
if (excludeLinkTextField) {
parsedQs.excludeLinkTextField = true;
}
if (inHistoryViewer) {
parsedQs.inHistoryViewer = '1';
}
for (const prop of ['href', 'path', 'pathname']) {
parsedURL[prop] = joinUrlPaths(parsedURL[prop], linkID.toString());
}
Expand Down
1 change: 1 addition & 0 deletions client/src/containers/LinkModalContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const LinkModalContainer = ({ types, typeKey, linkID = 0, isOpen, onSuccess, onC
// which will causes bugs with validation
if (!LinkModal) {
setLinkModal(() => loadComponent(`LinkModal.${handlerName}`));
return;
Copy link
Member Author

@emteknetnz emteknetnz Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should is to fix a console error where the state hasn't yet been updated from the call the setLinkModal, meaning that the <LinkModal> jsx object doesn't yet exist at time of render because LinkModal hasn't yet been set. Setting the state via this hook will trigger a rerender in which time <LinkModal> will not exist

}

return <LinkModal
Expand Down
1 change: 1 addition & 0 deletions client/src/entwine/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jQuery.entwine('ss', ($) => {
canCreate: inputField.data('can-create') ? true : false,
readonly: inputField.data('readonly') ? true : false,
disabled: inputField.data('disabled') ? true : false,
inHistoryViewer: inputField.data('in-history-viewer') ? true : false,
};
},

Expand Down
4 changes: 3 additions & 1 deletion src/Controllers/LinkFieldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,9 @@ private function createLinkForm(Link $link, string $operation, bool $excludeLink
// Make readonly if fail can check
if ($operation === 'create' && !$link->canCreate()
|| $operation === 'edit' && !$link->canEdit()
|| $this->getFieldIsReadonlyOrDisabled()) {
|| $this->getFieldIsReadonlyOrDisabled()
|| $this->getRequest()->getVar('inHistoryViewer')
) {
$form->makeReadonly();
}

Expand Down
8 changes: 8 additions & 0 deletions src/Form/AbstractLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use SilverStripe\LinkField\Models\Link;
use SilverStripe\LinkField\Services\LinkTypeService;
use SilverStripe\ORM\DataObject;
use SilverStripe\VersionedAdmin\Controllers\HistoryViewerController;

/**
* Abstract form field for managing Link records
Expand Down Expand Up @@ -112,6 +113,7 @@ public function getSchemaDataDefaults(): array
$data = parent::getSchemaDataDefaults();
$data['types'] = json_decode($this->getTypesProp());
$data['excludeLinkTextField'] = $this->getExcludeLinkTextField();
$data['inHistoryViewer'] = $this->getInHistoryViewer();
$ownerFields = $this->getOwnerFields();
$data['ownerID'] = $ownerFields['ID'];
$data['ownerClass'] = $ownerFields['Class'];
Expand All @@ -136,13 +138,19 @@ protected function getDefaultAttributes(): array
$attributes['data-readonly'] = $this->isReadonly();
$attributes['data-disabled'] = $this->isDisabled();
$attributes['data-exclude-linktext-field'] = $this->getExcludeLinkTextField();
$attributes['data-in-history-viewer'] = $this->getInHistoryViewer();
$ownerFields = $this->getOwnerFields();
$attributes['data-owner-id'] = $ownerFields['ID'];
$attributes['data-owner-class'] = $ownerFields['Class'];
$attributes['data-owner-relation'] = $ownerFields['Relation'];
return $attributes;
}

private function getInHistoryViewer(): bool
{
return is_a($this->getForm()->getController(), HistoryViewerController::class);
}

protected function getOwner(): DataObject
{
/** @var Form $form */
Expand Down
Loading