diff --git a/client/src/components/LinkField/LinkField-story.js b/client/src/components/LinkField/LinkField-story.js
new file mode 100644
index 00000000..f91534db
--- /dev/null
+++ b/client/src/components/LinkField/LinkField-story.js
@@ -0,0 +1,159 @@
+/* global window */
+import React from 'react';
+import { Component as LinkField } from 'components/LinkField/LinkField';
+
+// mock global ss config
+if (!window.ss) {
+ window.ss = {};
+}
+if (!window.ss.config) {
+ window.ss.config = {
+ sections: [
+ {
+ name: 'SilverStripe\\LinkField\\Controllers\\LinkFieldController',
+ form: {
+ linkForm: {
+ dataUrl: 'linkfield-endpoint',
+ }
+ }
+ },
+ ]
+ };
+}
+
+// mock toast actions
+const mockedActions = {
+ toasts: {
+ error: (text) => console.error(text),
+ success: (text) => console.warn(text),
+ }
+};
+
+// predetermine link types
+const linkTypes = {
+ sitetree: {
+ key: 'sitetree',
+ title: 'Page on this site',
+ handlerName: 'FormBuilderModal',
+ priority: 0,
+ icon: 'font-icon-page',
+ allowed: true
+ },
+ file: {
+ key: 'file',
+ title: 'Link to a file',
+ handlerName: 'FormBuilderModal',
+ priority: 10,
+ icon: 'font-icon-image',
+ allowed: true
+ },
+ external: {
+ key: 'external',
+ title: 'Link to external URL',
+ handlerName: 'FormBuilderModal',
+ priority: 20,
+ icon: 'font-icon-external-link',
+ allowed: true
+ },
+ email: {
+ key: 'email',
+ title: 'Link to email address',
+ handlerName: 'FormBuilderModal',
+ priority: 30,
+ icon: 'font-icon-p-mail',
+ allowed: true
+ },
+ phone: {
+ key: 'phone',
+ title: 'Phone number',
+ handlerName: 'FormBuilderModal',
+ priority: 40,
+ icon: 'font-icon-mobile',
+ allowed: true
+ }
+};
+
+export default {
+ title: 'Linkfield/LinkField',
+ component: LinkField,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ description: {
+ component: 'The LinkField component. Note that the form modal for creating, editing, and viewing link data is disabled for the storybook.'
+ },
+ canvas: {
+ sourceState: 'shown',
+ },
+ controls: {
+ exclude: [
+ 'onChange',
+ 'value',
+ 'ownerID',
+ 'ownerClass',
+ 'ownerRelation',
+ 'actions',
+ ],
+ },
+ },
+ },
+ argTypes: {
+ types: {
+ description: 'Types of links that are allowed in this field. The actual prop is a JSON object with some metadata about each type.',
+ control: 'inline-check',
+ options: Object.keys(linkTypes),
+ },
+ isMulti: {
+ description: 'Whether the field supports multiple links or not.',
+ },
+ canCreate: {
+ description: 'Whether the current user has create permission or not.',
+ },
+ readonly: {
+ description: 'Whether the field is readonly or not.',
+ },
+ disabled: {
+ description: 'Whether the field is disabled or not.',
+ },
+ ownerSaved: {
+ description: 'Whether the record which owns the link field has been saved or not. The actual props for this are OwnerID, OwnerClass, and OwnerRelation.',
+ },
+ },
+};
+
+export const _LinkField = {
+ name: 'LinkField',
+ args: {
+ value: [0],
+ onChange: () => {},
+ types: Object.keys(linkTypes),
+ actions: mockedActions,
+ isMulti: false,
+ canCreate: true,
+ readonly: false,
+ disabled: false,
+ ownerSaved: true,
+ ownerClass: '',
+ ownerRelation: '',
+ },
+ render: (args) => {
+ const { types, ownerSaved } = args;
+ delete args.ownerSaved;
+ delete args.hasLinks;
+
+ // `types` must be an array in args so controls can be used to toggle them.
+ // Because of that, we need to turn that back into the JSON object before
+ // passing that prop.
+ args.types = {};
+ types.sort((a, b) => linkTypes[a].priority - linkTypes[b].priority);
+ // eslint-disable-next-line no-restricted-syntax
+ for (const type of types) {
+ args.types[type] = linkTypes[type];
+ }
+
+ // Determine whether the link is rendered as though the parent record is saved or not
+ args.ownerID = ownerSaved ? 1 : 0;
+
+ return ;
+ },
+};
diff --git a/client/src/components/LinkPicker/LinkPickerTitle-story.js b/client/src/components/LinkPicker/LinkPickerTitle-story.js
new file mode 100644
index 00000000..8fc871de
--- /dev/null
+++ b/client/src/components/LinkPicker/LinkPickerTitle-story.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import LinkPickerTitle from 'components/LinkPicker/LinkPickerTitle';
+import { LinkFieldContext } from '../LinkField/LinkField';
+
+// // mock global ss config
+// if (!window.ss) {
+// window.ss = {};
+// }
+// if (!window.ss.config) {
+// window.ss.config = {
+// sections: [
+// {
+// name: 'SilverStripe\\LinkField\\Controllers\\LinkFieldController',
+// form: {
+// linkForm: {
+// dataUrl: 'linkfield-endpoint',
+// }
+// }
+// },
+// ]
+// };
+// }
+
+// mock toast actions
+const mockedActions = {
+ toasts: {
+ error: (text) => console.error(text),
+ success: (text) => console.warn(text),
+ }
+};
+
+export default {
+ title: 'LinkField/LinkPicker/LinkPickerTitle',
+ component: LinkPickerTitle,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ description: {
+ component: 'The LinkPickerTitle component. Used to display a link inside the link field'
+ },
+ canvas: {
+ sourceState: 'shown',
+ },
+ controls: {
+ exclude: [
+ 'id',
+ 'onDelete',
+ 'onClick',
+ 'onUnpublishedVersionedState',
+ 'isFirst',
+ 'isLast',
+ 'isSorting',
+ 'canCreate',
+ ],
+ },
+ },
+ },
+ argTypes: {
+ versionState: {
+ description: 'The current versioned state of the link. "unsaved" and "unversioned" are effectively identical.',
+ control: 'select',
+ options: ['unversioned', 'unsaved', 'published', 'draft', 'modified'],
+ },
+ title: {
+ description: 'The title (aka link text) for the link.',
+ },
+ typeTitle: {
+ description: 'Text that informs the user what type of link this is.',
+ },
+ description: {
+ description: 'The URL, or information about what the link is linking to.',
+ },
+ typeIcon: {
+ description: 'CSS class of an icon for this type of link (usually prefixed with "font-icon-"). See the Admin/Icons story for the full set of options.',
+ },
+ isMulti: {
+ description: 'Whether this link is inside a link field that supports multiple links or not.',
+ },
+ canDelete: {
+ description: 'Whether the current user has the permissions necessary to delete (or archive) this link.',
+ },
+ readonly: {
+ description: 'Whether the link field is readonly.',
+ },
+ disabled: {
+ description: 'Whether the link field is disabled.',
+ },
+ loading: {
+ description: 'Whether the link field is loading. This is passed as part of the context, not as a prop, but is here for demonstration purposes.',
+ },
+ },
+};
+
+export const _LinkPickerTitle = {
+ name: 'LinkPickerTitle',
+ args: {
+ id: 1,
+ title: 'Example link',
+ typeTitle: 'External URL',
+ description: 'https://www.example.com',
+ typeIcon: 'font-icon-external-link',
+ versionState: 'unversioned',
+ onDelete: () => {},
+ onClick: () => {},
+ onUnpublishedVersionedState: () => {},
+ isFirst: true,
+ isLast: true,
+ isMulti: false,
+ canCreate: true,
+ canDelete: true,
+ isSorting: false,
+ readonly: false,
+ disabled: false,
+ loading: false,
+ },
+ render: (args) => {
+ const providerArgs = {
+ ownerID: 1,
+ ownerClass: '',
+ ownerRelation: '',
+ actions: mockedActions,
+ loading: args.loading,
+ };
+ delete args.loading;
+ return
+
+ ;
+ }
+};