Skip to content

Commit

Permalink
Merge branch '4.0' into 4
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Mar 18, 2024
2 parents 746cb83 + 967c457 commit 5bb148d
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 28 deletions.
8 changes: 8 additions & 0 deletions _config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ SilverStripe\Admin\LeftAndMain:
SilverStripe\Admin\Forms\UsedOnTable:
extensions:
- SilverStripe\LinkField\Extensions\UsedOnTableExtension

---
Only:
moduleexists: 'tractorcow/silverstripe-fluent'
---
SilverStripe\LinkField\Models\Link:
extensions:
- SilverStripe\LinkField\Extensions\FluentLinkExtension
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

19 changes: 13 additions & 6 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import i18n from 'i18n';
import url from 'url';
import qs from 'qs';
import classnames from 'classnames';
import versionStates from 'constants/versionStates';

export const LinkFieldContext = createContext(null);

Expand Down Expand Up @@ -267,7 +268,11 @@ const LinkField = ({
*/
const handleDelete = (linkID, deleteType) => {
const versionState = data[linkID]?.versionState || '';
const isVersioned = ['draft', 'modified', 'published'].includes(versionState);
const isVersioned = [
versionStates.draft,
versionStates.modified,
versionStates.published
].includes(versionState);
const deleteText = isVersioned
? i18n._t('LinkField.ARCHIVE_CONFIRM', 'Are you sure you want to archive this link?')
: i18n._t('LinkField.DELETE_CONFIRM', 'Are you sure you want to delete this link?');
Expand Down Expand Up @@ -348,12 +353,14 @@ const LinkField = ({
const links = [];
for (let i = 0; i < linkIDs.length; i++) {
const linkID = linkIDs[i];
// Only render items we have data for
const linkData = data[linkID];
if (!linkData) {
// Render dataless item to provide a good loading experience, except if we have single link field
const linkData = data[linkID] || {};
if (!linkData && !isMulti) {
continue;
}
const type = types.hasOwnProperty(linkData.typeKey) ? types[linkData.typeKey] : {};
const type = types.hasOwnProperty(linkData.typeKey) ?
types[linkData.typeKey] :
{icon: 'font-icon-link' };
links.push(<LinkPickerTitle
key={linkID}
id={linkID}
Expand Down Expand Up @@ -446,7 +453,7 @@ const LinkField = ({

const saveRecordFirst = !loadingError && ownerID === 0;
const renderLoadingError = loadingError;
const renderPicker = !loadingError && !inHistoryViewer && !saveRecordFirst && (isMulti || Object.keys(data).length === 0);
const renderPicker = !loadingError && !inHistoryViewer && !saveRecordFirst && (isMulti || linkIDs.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');
Expand Down
22 changes: 20 additions & 2 deletions client/src/components/LinkField/tests/LinkField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,30 @@ 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
})}
/>);
await doResolve({ json: () => ({
123: {
title: 'Page title',
typeKey: 'mylink',
}
}) });
await screen.findByText('Page title');
const linkPicker = container.querySelectorAll('#link-picker__link-123');
expect(linkPicker[0]).toHaveAttribute('aria-disabled');
expect(linkPicker[0]).toHaveClass('link-picker__link--disabled');
});

test('Empty disabled LinkField will display cannot edit message', async () => {
const { container } = render(<LinkField {...makeProps({
ownerID: 1,
disabled: true,
value: undefined
})}
/>);
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 @@ -176,7 +194,7 @@ test('LinkField will render loading indicator if ownerID is not 0', async () =>
/>);
expect(container.querySelectorAll('.link-field__save-record-first')).toHaveLength(0);
expect(container.querySelectorAll('.link-field__loading')).toHaveLength(1);
expect(container.querySelectorAll('.link-picker')).toHaveLength(1);
expect(container.querySelectorAll('.link-picker')).toHaveLength(0);
});

test('LinkField will render link-picker if ownerID is not 0 and isMulti and has finished loading', async () => {
Expand Down
25 changes: 16 additions & 9 deletions client/src/components/LinkPicker/LinkPickerTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LinkFieldContext } from 'components/LinkField/LinkField';
import { Button } from 'reactstrap';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import versionStates from 'constants/versionStates';

const stopPropagation = (fn) => (e) => {
e.nativeEvent.stopImmediatePropagation();
Expand All @@ -19,10 +20,10 @@ const stopPropagation = (fn) => (e) => {
const getVersionedBadge = (versionState) => {
let title = '';
let label = ''
if (versionState === 'draft') {
if (versionState === versionStates.draft) {
title = i18n._t('LinkField.LINK_DRAFT_TITLE', 'Link has draft changes');
label = i18n._t('LinkField.LINK_DRAFT_LABEL', 'Draft');
} else if (versionState === 'modified') {
} else if (versionState === versionStates.modified) {
title = i18n._t('LinkField.LINK_MODIFIED_TITLE', 'Link has unpublished changes');
label = i18n._t('LinkField.LINK_MODIFIED_LABEL', 'Modified');
} else {
Expand Down Expand Up @@ -108,11 +109,11 @@ const LinkPickerTitle = ({
classes[`link-picker__link--${versionState}`] = true;
}
const className = classnames(classes);
const deleteText = ['unversioned', 'unsaved'].includes(versionState)
const deleteText = [versionStates.unversioned, versionStates.unsaved].includes(versionState)
? i18n._t('LinkField.DELETE', 'Delete')
: i18n._t('LinkField.ARCHIVE', 'Archive');
const ariaLabel = i18n._t('LinkField.EDIT_LINK', 'Edit link');
if (['draft', 'modified'].includes(versionState)) {
if ([versionStates.draft, versionStates.modified].includes(versionState)) {
onUnpublishedVersionedState();
}
// Remove the default tabindex="0" attribute from the sortable element because we're going to manually
Expand Down Expand Up @@ -155,10 +156,12 @@ const LinkPickerTitle = ({
<span className="link-picker__title-text">{title}</span>
{getVersionedBadge(versionState)}
</div>
<small className="link-picker__type">
{typeTitle}:&nbsp;
<span className="link-picker__url">{description}</span>
</small>
{typeTitle && (
<small className="link-picker__type">
{typeTitle}:&nbsp;
<span className="link-picker__url">{description}</span>
</small>
)}
</div>
{(canDelete && !readonly && !disabled) &&
// This is a <span> rather than a <Button> because we're inside a <Button> and
Expand All @@ -180,7 +183,7 @@ LinkPickerTitle.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string,
description: PropTypes.string,
versionState: PropTypes.string,
versionState: PropTypes.oneOf(Object.values(versionStates)),
typeTitle: PropTypes.string.isRequired,
typeIcon: PropTypes.string.isRequired,
onDelete: PropTypes.func.isRequired,
Expand All @@ -198,4 +201,8 @@ LinkPickerTitle.propTypes = {
buttonRef: PropTypes.object.isRequired,
};

LinkPickerTitle.defaultProps = {
versionState: versionStates.unversioned,
}

export default LinkPickerTitle;
9 changes: 9 additions & 0 deletions client/src/constants/versionStates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const versionStates = {
draft: 'draft',
modified: 'modified',
unversioned: 'unversioned',
unsaved: 'unsaved',
published: 'published',
};

export default versionStates;
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"php": "^8.1",
"silverstripe/framework": "^5.2",
"silverstripe/cms": "^5",
"silverstripe/versioned": "^2"
"silverstripe/versioned": "^2",
"silverstripe/admin": "^2.2"
},
"require-dev": {
"silverstripe/frameworktest": "^1",
Expand Down
10 changes: 8 additions & 2 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ en:
CREATE_LINK: 'Create link'
MENUTITLE: 'Link fields'
UPDATE_LINK: 'Update link'
SilverStripe\LinkField\Form\AbstractLinkField:
INVALID_TYPECLASS_EMPTY: '"{class}": Allowed types cannot be empty'
SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait:
INVALID_TYPECLASS: '"{class}": {typeclass} is not a valid Link Type'
INVALID_TYPECLASS_EMPTY: '"{class}": Allowed types cannot be empty'
Expand Down Expand Up @@ -38,8 +40,8 @@ en:
SINGULARNAME: 'File Link'
has_one_File: File
SilverStripe\LinkField\Models\Link:
LINK_TEXT_TITLE: 'Link text'
LINK_TEXT_TEXT_DESCRIPTION: 'If left blank, an appropriate default will be used on the front-end'
LINK_TEXT_TITLE: 'Link text'
LINK_TYPE_TITLE: 'Link Type'
MISSING_DEFAULT_TITLE: '(No value provided)'
OPEN_IN_NEW_TITLE: 'Open in new window?'
Expand All @@ -48,10 +50,14 @@ en:
one: 'A Link'
other: '{count} Links'
SINGULARNAME: Link
db_OpenInNew: 'Open in new'
db_LinkText: 'Link text'
db_OpenInNew: 'Open in new'
db_Sort: Sort
db_Version: Version
has_one_Owner: Owner
SilverStripe\LinkField\Models\MultiLinkField:
AddLink: 'Add link'
EditLink: 'Edit link'
SilverStripe\LinkField\Models\PhoneLink:
LINKLABEL: 'Phone number'
PHONE_FIELD: Phone
Expand Down
35 changes: 35 additions & 0 deletions src/Extensions/FluentLinkExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace SilverStripe\LinkField\Extensions;

use SilverStripe\Core\Extension;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\FieldList;
use TractorCow\Fluent\Extension\FluentVersionedExtension;

class FluentLinkExtension extends Extension
{
public function updateCMSFields(FieldList $fields)
{
$this->removeTabsWithFluentGridfields($fields);
}

/**
* Remove tabs added by fluent that contain GridFields
* This is done for because there is currently no react component for a GridFields
* When using tractorcow/silverstripe-fluent tabs will be automatically added
* that contain a GridFields which will cause LinkField modal to trigger a server error
*/
private function removeTabsWithFluentGridfields(FieldList $fields): void
{
// Only remove tabs if there is no GridField react component specified
$schemaDataType = GridField::create('tmp')->getSchemaDataType();
if ($schemaDataType !== null) {
return;
}
// Remove the tabs that contains the gridfields
if ($this->getOwner()->hasExtension(FluentVersionedExtension::class)) {
$fields->removeByName(['Locales', 'FilteredLocales']);
}
}
}
6 changes: 6 additions & 0 deletions src/Form/MultiLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace SilverStripe\LinkField\Form;

use LogicException;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Relation;
use SilverStripe\ORM\SS_List;
Expand Down Expand Up @@ -132,4 +133,9 @@ private function loadFrom(DataObject $record): void
$value = array_values($relation->getIDList() ?? []);
parent::setValue($value);
}

public function LinkIDs(): ArrayList
{
return ArrayList::create($this->dataValue());
}
}
3 changes: 1 addition & 2 deletions src/Models/FileLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ protected function getDefaultTitle(): string
if (!$file?->exists()) {
return _t(__CLASS__ . '.MISSING_DEFAULT_TITLE', '(File missing)');
}

return (string) $this->getDescription();
return $file->Title;
}
}
15 changes: 14 additions & 1 deletion templates/SilverStripe/LinkField/Form/LinkField.ss
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
<%-- This template is here to bootstrap a LinkField React form field --%>
<%-- It includes some pre-rendered content to provide a nicer UI while waiting for React to boot --%>
<%-- Once React is done pre-rendering, it will discard the pre-rendered markup --%>
<input $AttributesHTML />
<div data-field-id="$ID" data-schema-component="$SchemaComponent" class="entwine-linkfield" data-types="$TypesProp"></div>
<div data-field-id="$ID" data-schema-component="$SchemaComponent" class="entwine-linkfield" data-types="$TypesProp">
<div class="link-field__container">
<% include SilverStripe/LinkField/Form/LinkField_Spinner %>
<div>
<div class="link-picker__link link-picker__link--is-first link-picker__link--is-last form-control link-picker__link--disabled link-picker__link--published" role="button" aria-disabled="false" aria-roledescription="sortable" aria-describedby="" id="link-picker__link-42">
<button type="button" disabled="" class="link-picker__button font-icon-link btn btn-secondary disabled"></button>
</div>
</div>
</div>
</div>

9 changes: 9 additions & 0 deletions templates/SilverStripe/LinkField/Form/LinkField_Spinner.ss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%-- This template is here to bootstrap a LinkField React form field --%>
<%-- It includes a pre-rendered spinner to provide a nicer UI while waiting for React to boot --%>
<%-- Once React is done pre-rendering, it will discard the pre-rendered markup --%>
<div class="link-field__loading">
<div class="cms-content-loading-overlay ui-widget-overlay-light"></div>
<% include SilverStripe/Admin/Includes/CMSLoadingSpinner %>
</div>


35 changes: 34 additions & 1 deletion templates/SilverStripe/LinkField/Form/MultiLinkField.ss
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
<%-- This template is here to bootstrap a MultiLinkField React form field --%>
<%-- It includes some pre-rendered content to provide a nicer UI while waiting for React to boot --%>
<%-- Once React is done pre-rendering, it will discard the pre-rendered markup --%>
<input $AttributesHTML />
<div data-is-multi="true" data-field-id="$ID" data-schema-component="$SchemaComponent" class="entwine-linkfield" data-types="$TypesProp"></div>
<div data-is-multi="true" data-field-id="$ID" data-schema-component="$SchemaComponent" class="entwine-linkfield" data-types="$TypesProp">

<div class="link-field__container">
<% include SilverStripe/LinkField/Form/LinkField_Spinner %>
<div class="link-picker form-control">
<div class="link-picker__menu dropdown">
<button
type="button"
aria-haspopup="true"
aria-expanded="false"
class="link-picker__menu-toggle font-icon-plus-1 dropdown-toggle btn btn-secondary"
aria-label="<%t SilverStripe\LinkField\Models\MultiLinkField.AddLink "Add link" %>">
<%t SilverStripe\LinkField\Models\MultiLinkField.AddLink "Add link" %>
</button>
</div>
</div>
<div class="link-picker-links">
<% loop $LinkIDs %>
<div class="link-picker__link form-control link-picker__link--published <% if $IsFirst %>link-picker__link--is-first<% end_if %> <% if $IsLast %>link-picker__link--is-last<% end_if %>">
<button
type="button"
disabled=""
class="link-picker__button font-icon-link btn btn-secondary disabled"
aria-label="<%t SilverStripe\LinkField\Models\MultiLinkField.EditLink "Edit link" %>"></button>
</div>
<% end_loop %>
</div>
</div>

</div>

1 change: 1 addition & 0 deletions tests/behat/features/accessibility-linkfield.feature
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Feature: Accessibility Tests

Then I press the "Tab" key globally
And I press the "Tab" key globally
And I press the "Tab" key globally
And I press the "Enter" key globally
And I should see "Page on this site" in the "[data-field-id='Form_EditForm_HasManyLinks'] .dropdown-menu.show" element
And I press the "Down" key globally
Expand Down
3 changes: 2 additions & 1 deletion tests/behat/features/create-edit-linkfield.feature
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ I want to add links to pages, files, external URLs, email addresses and phone nu

# Fourth link in multi link field
And I should see "Link to a file" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link:nth-of-type(4)" element
And I should see "folder1/file1.jpg" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link:nth-of-type(4)" element
And I should see "File1" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link:nth-of-type(4)" element
And I should see "Draft" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link:nth-of-type(4)" element

# Test that user can publish the page with links
Expand Down Expand Up @@ -203,5 +203,6 @@ I want to add links to pages, files, external URLs, email addresses and phone nu
And I wait for 2 seconds

And I should see "Link to a file" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link--is-first" element
And I should see "File1" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link--is-first" element
And I should see "folder1/file1.jpg" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link--is-first" element
And I should see "Draft" in the "[data-field-id='Form_EditForm_HasManyLinks'] .link-picker__link--is-first" element
3 changes: 2 additions & 1 deletion tests/php/Models/FileLinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function testGetDefaultTitle(): void
$link = new FileLink();
$this->assertSame('(File missing)', $reflectionGetDefaultTitle->invoke($link));
// File exists in DB but not in filesystem
// Note that a 'Title' field will be derived from the 'Name' field in File::onBeforeWrite()
$file = new TestFileCanView(['Name' => 'My test file']);
$file->write();
$link->File = $file->ID;
Expand All @@ -56,6 +57,6 @@ public function testGetDefaultTitle(): void
$file->write();
$link->File = $file->ID;
$link->write();
$this->assertSame('file-b.png', $reflectionGetDefaultTitle->invoke($link));
$this->assertSame('My test file', $reflectionGetDefaultTitle->invoke($link));
}
}
Loading

0 comments on commit 5bb148d

Please sign in to comment.