@financial-times/n-myft-ui 25.0.0 → 26.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import CsrfToken from '../csrf-token/input';
3
3
  import FollowButton from '../follow-button/follow-button';
4
4
 
5
- export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [] }) {
5
+ export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [], csrfToken, cacheablePersonalisedUrl }) {
6
6
  const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
7
7
  let formProps = {
8
8
  method: 'POST',
@@ -27,7 +27,7 @@ export default function Collections ({ title, liteStyle, flags, collectionName,
27
27
  <ul className="collection__concepts">
28
28
  {concepts && concepts.map((concept, index) =>
29
29
  <li className="collection__concept" key={index}>
30
- <FollowButton variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
30
+ <FollowButton cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
31
31
  </li>)
32
32
  }
33
33
  </ul>
@@ -41,7 +41,7 @@ export default function Collections ({ title, liteStyle, flags, collectionName,
41
41
  name="directType"
42
42
  value={concepts.map(concept => concept.directType).join(',')}
43
43
  />
44
- <CsrfToken />
44
+ <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
45
45
  <input
46
46
  type="hidden"
47
47
  name="name"
@@ -1,24 +1,32 @@
1
1
  import React, { Fragment } from 'react';
2
2
  import FollowButton from '../follow-button/follow-button';
3
3
 
4
- export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable }) {
4
+ export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable, csrfToken, cacheablePersonalisedUrl }) {
5
5
 
6
6
  const {
7
7
  myFtApi,
8
8
  myFtApiWrite
9
9
  } = flags;
10
10
 
11
- const generateTrackableProps = (primary, seconday) => {
11
+ const generateTrackableProps = (primary, secondary) => {
12
12
  return {
13
- 'data-trackable': primary ? primary : seconday
13
+ 'data-trackable': primary ? primary : secondary
14
14
  }
15
15
  }
16
16
 
17
+ const shouldDisplay = () => {
18
+ if(myFtApi && myFtApiWrite && Array.isArray(concepts) && concepts.length) {
19
+ return true
20
+ }
21
+
22
+ return false;
23
+ }
24
+
17
25
 
18
26
  return (
19
27
 
20
28
  <Fragment>
21
- {(myFtApi && myFtApiWrite && concepts && concepts.length) &&
29
+ {shouldDisplay() &&
22
30
  <div
23
31
  className='concept-list'
24
32
  {...generateTrackableProps(trackable, 'concept-list')}>
@@ -39,13 +47,19 @@ export default function ConceptList ({ flags, concepts, contentType, conceptList
39
47
  } = concept;
40
48
  return (
41
49
  <li key={index} className='concept-list__list-item'>
50
+ {/* The relativeUrl and url point to the same resource. The url is the base path + the relative url.
51
+ Example: browser_path = https://ft.com, relativeUrl = /capital-markets then url = https://www.ft.com/capital-markets.
52
+
53
+ Note: we don't need to compute these urls in the business logic of these components as they're passed in as props.
54
+
55
+ This note is just an explanation for why relativeUrl has preference over url.*/}
42
56
  <a
43
57
  href={relativeUrl || url}
44
58
  {...generateTrackableProps(conceptTrackable, 'concept')}
45
59
  className='concept-list__concept'>
46
60
  {prefLabel}
47
61
  </a>
48
- <FollowButton conceptId={id} name={prefLabel} flags={flags} />
62
+ <FollowButton csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} conceptId={id} name={prefLabel} flags={flags} />
49
63
  </li>
50
64
  )
51
65
  })}
@@ -5,18 +5,12 @@ export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
5
5
  let inputProps = {};
6
6
 
7
7
  if (cacheablePersonalisedUrl) {
8
- inputProps = {
9
- ...inputProps,
10
- 'data-myft-csrf-token': csrfToken
11
- };
12
- }
13
-
14
- if(csrfToken) {
15
8
  inputProps.value = csrfToken;
16
9
  }
17
10
 
18
11
  return (
19
12
  <input
13
+ data-myft-csrf-token
20
14
  {...inputProps}
21
15
  type="hidden"
22
16
  name="token"
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import CsrfToken from '../input';
2
+ import CsrfToken from './input';
3
3
  import { render } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
 
@@ -16,7 +16,7 @@ describe('Csrf Token Input', () => {
16
16
 
17
17
  test('It renders csrf token attribute', async () => {
18
18
  let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
19
- expect(container.querySelector('[data-myft-csrf-token=\'test-token\']')).toBeTruthy();
19
+ expect(container.querySelector('[data-myft-csrf-token]')).toBeTruthy();
20
20
  });
21
21
 
22
22
 
@@ -140,6 +140,8 @@ export default function FollowButton (props) {
140
140
  extraClasses,
141
141
  conceptId,
142
142
  variant,
143
+ csrfToken,
144
+ cacheablePersonalisedUrl
143
145
  } = props;
144
146
 
145
147
  const formProps = generateFormProps(props);
@@ -155,7 +157,7 @@ export default function FollowButton (props) {
155
157
  data-myft-ui="follow"
156
158
  data-concept-id={conceptId}
157
159
  {...formProps}>
158
- <CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
160
+ <CsrfToken cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} />
159
161
  <div
160
162
  className="n-myft-ui__announcement o-normalise-visually-hidden"
161
163
  aria-live="assertive"
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import FollowButton from '../follow-button';
2
+ import FollowButton from './follow-button';
3
3
  import { render, screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
 
@@ -4,6 +4,7 @@ import ConceptList from './concept-list/concept-list';
4
4
  import Collections from './collections/collections';
5
5
  import SaveForLater from './save-for-later/save-for-later';
6
6
  import PinButton from './pin-button/pin-button';
7
+ import InstantAlert from './instant-alert/instant-alert';
7
8
 
8
9
  export {
9
10
  CsrfToken,
@@ -11,5 +12,6 @@ export {
11
12
  ConceptList,
12
13
  Collections,
13
14
  SaveForLater,
14
- PinButton
15
+ PinButton,
16
+ InstantAlert
15
17
  };
@@ -0,0 +1,73 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ /**
5
+ *
6
+ * @param {Object} props
7
+ * @param {string} props.name
8
+ * @param {Object} props.flags
9
+ * @param {booelan} props.hideButtonText
10
+ * @param {string} props.conceptId
11
+ * @param {string} props.name
12
+ * @param {string} props.extraClasses
13
+ * @param {boolean} props.directType
14
+ * @param {string} props.cacheablePersonalisedUrl
15
+ * @param {string} props.hasInstantAlert
16
+ * @param {string} props.buttonText
17
+ * @param {string} props.alternateText
18
+ * @param {string} props.variant
19
+ * @param {string} props.size
20
+ */
21
+ export default function InstantAlert (props) {
22
+
23
+ const {
24
+ hasInstantAlert,
25
+ cacheablePersonalisedUrl,
26
+ name,
27
+ alternateText,
28
+ buttonText,
29
+ conceptId,
30
+ variant,
31
+ size,
32
+ flags,
33
+ hideButtonText,
34
+ directType,
35
+ extraClasses
36
+ } = props;
37
+
38
+ const generateButtonProps = () => {
39
+
40
+ let buttonProps = {
41
+ 'aria-pressed': `${Boolean(hasInstantAlert) && Boolean(cacheablePersonalisedUrl)}`,
42
+ 'aria-label': `Get instant alerts for ${name}`,
43
+ 'data-alternate-label': `Stop instant alerts for ${name}`,
44
+ 'data-alternate-text': alternateText? alternateText: (buttonText ? buttonText : 'Instant alerts'),
45
+ 'data-concept-id': conceptId, // duplicated here for tracking
46
+ 'data-trackable': 'instant',
47
+ title: `Get instant alerts for ${name}`,
48
+ value: hasInstantAlert ? false : true,
49
+ type: 'submit',
50
+ name: '_rel.instant',
51
+ className: `n-myft-ui__button n-myft-ui__button--instant n-myft-ui__button--instant-light${variant ? ` n-myft-ui__button--${variant}` : ''}${size ? ` n-myft-ui__button--${size}` : ''}`
52
+ };
53
+ return buttonProps;
54
+ }
55
+
56
+ return (
57
+ <Fragment>
58
+ {flags.myFtApiWrite &&
59
+ <form className={`n-myft-ui n-myft-ui--instant${hideButtonText ? ' n-myft-ui--instant--hide-text' : ''}${extraClasses ? ` ${extraClasses}` : ''}`}
60
+ method="GET"
61
+ data-myft-ui="instant"
62
+ data-concept-id={conceptId}
63
+ action={`/myft/add/${conceptId}?instant=true`}
64
+ data-js-action={`/__myft/api/core/followed/concept/${conceptId}?method=put`}>
65
+ <CsrfToken />
66
+ <input type="hidden" value={name} name="name" />
67
+ <input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
68
+ <button {...generateButtonProps()}>{buttonText ? buttonText : 'Instant alerts'}</button>
69
+ </form>}
70
+ </Fragment>
71
+ );
72
+
73
+ }
@@ -0,0 +1,86 @@
1
+ import React from 'react';
2
+ import InstantAlert from './instant-alert';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const props = {
7
+ flags: {
8
+ myFtApi: true,
9
+ myFtApiWrite: true
10
+ },
11
+ conceptId: '0000-000000-00000-0000',
12
+ name: 'Instant Alert',
13
+ buttonText: 'Instant Alert'
14
+ };
15
+
16
+ describe('InstantAlert', () => {
17
+
18
+ test('It renders', async () => {
19
+ render(<InstantAlert {...props} />);
20
+ expect(await screen.findByText('Instant Alert')).toBeInTheDocument();
21
+ });
22
+
23
+ test('It renders form attributes', () => {
24
+ const { container } = render(<InstantAlert {...props} />);
25
+ expect(container.querySelector('form[method="GET"]')).toBeInTheDocument();
26
+ expect(container.querySelector(`form[action='/myft/add/${props.conceptId}?instant=true']`)).toBeInTheDocument();
27
+ expect(container.querySelector('form[data-myft-ui="instant"]')).toBeInTheDocument();
28
+ expect(container.querySelector(`form[data-concept-id="${props.conceptId}"]`)).toBeInTheDocument();
29
+ expect(container.querySelector(`form[data-js-action="/__myft/api/core/followed/concept/${props.conceptId}?method=put"]`)).toBeInTheDocument();
30
+ });
31
+
32
+ test('It renders extraClasses in form', () => {
33
+ const { container } = render(<InstantAlert {...props} extraClasses={'extra'} />);
34
+ expect(container.querySelector('form[class="n-myft-ui n-myft-ui--instant extra"]')).toBeInTheDocument();
35
+ });
36
+
37
+ test('It renders hide button class in form', () => {
38
+ const { container } = render(<InstantAlert {...props} hideButtonText={true} />);
39
+ expect(container.querySelector('form[class="n-myft-ui n-myft-ui--instant n-myft-ui--instant--hide-text"]')).toBeInTheDocument();
40
+ });
41
+
42
+ test('It renders csrftoken input', () => {
43
+ const { container } = render(<InstantAlert {...props} />);
44
+ expect(container.querySelector('input[data-myft-csrf-token]')).toBeInTheDocument();
45
+ });
46
+
47
+ test('It renders input name as value attribute', () => {
48
+ const { container } = render(<InstantAlert {...props} />);
49
+ expect(container.querySelector('input[value="Instant Alert"]')).toBeInTheDocument();
50
+ });
51
+
52
+ test('It renders input directType as value attribute', () => {
53
+ const { container } = render(<InstantAlert {...props} directType={'http://www.ft.com/ontology/test/Test'} />);
54
+ expect(container.querySelector('input[value="http://www.ft.com/ontology/test/Test"]')).toBeInTheDocument();
55
+ });
56
+
57
+ test('It renders button props', () => {
58
+ const { container } = render(<InstantAlert {...props} alternateText={'Sample alternate text'} variant={'blue'} size={'small'} />);
59
+ expect(container.querySelector(`button[aria-label="Get instant alerts for ${props.name}"]`)).toBeInTheDocument();
60
+ expect(container.querySelector('button[data-alternate-text="Sample alternate text"]')).toBeInTheDocument();
61
+ expect(container.querySelector(`button[data-concept-id="${props.conceptId}"]`)).toBeInTheDocument();
62
+ expect(container.querySelector('button[data-trackable="instant"]')).toBeInTheDocument();
63
+ expect(container.querySelector(`button[title="Get instant alerts for ${props.name}"]`)).toBeInTheDocument();
64
+ expect(container.querySelector('button[value="true"]')).toBeInTheDocument();
65
+ expect(container.querySelector('button[type="submit"]')).toBeInTheDocument();
66
+ expect(container.querySelector('button[name="_rel.instant"]')).toBeInTheDocument();
67
+ expect(container.getElementsByClassName('n-myft-ui__button--blue')).toHaveLength(1);
68
+ expect(container.getElementsByClassName('n-myft-ui__button--small')).toHaveLength(1);
69
+ });
70
+
71
+ test('It renders buttonText as data-alternate-text attribute when alternateText prop is not provided', () => {
72
+ const { container } = render(<InstantAlert {...props} buttonText={'Sample button text'} variant={'blue'} size={'small'} />);
73
+ expect(container.querySelector('button[data-alternate-text="Sample button text"]')).toBeInTheDocument();
74
+ });
75
+
76
+ test('It renders button aria-pressed=false attribute when hasInstantAlert=false or cacheablePersonalisedUrl props is not provided', () => {
77
+ render(<InstantAlert {...props} buttonText={'Sample button text'} hasInstantAlert={false} cacheablePersonalisedUrl={'https://ft.com'} />);
78
+ expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
79
+ });
80
+
81
+ test('It renders button aria-pressed=true attribute when hasInstantAlert=true and cacheablePersonalisedUrl provided', () => {
82
+ render(<InstantAlert {...props} buttonText={'Sample button text'} hasInstantAlert={true} cacheablePersonalisedUrl={'https://ft.com'} />);
83
+ expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
84
+ });
85
+
86
+ });
@@ -1,39 +1,39 @@
1
1
  import React, { Fragment } from 'react';
2
2
  import CsrfToken from '../csrf-token/input';
3
- export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised }) {
3
+ export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised, csrfToken, cacheablePersonalisedUrl }) {
4
4
 
5
- const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`
5
+ const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`;
6
+
7
+ if (!showPrioritiseButton) {
8
+ return null;
9
+ }
6
10
 
7
11
  return (
8
12
  <Fragment>
9
- {showPrioritiseButton &&
10
- <Fragment>
11
- <span className="myft-pin-divider"></span>
12
- <div className="myft-pin-button-wrapper">
13
- <form method="post" action={getAction()} data-myft-prioritise>
14
- <CsrfToken />
15
- <input type="hidden" value={name} name="name" />
16
- <input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
17
- <div
18
- className="n-myft-ui__announcement o-normalise-visually-hidden"
19
- aria-live="assertive"
20
- data-pressed-text={`${name} pinned in myFT.`}
21
- data-unpressed-text={`Unpinned ${name} from myFT.`}
22
- ></div>
23
- <button id={`myft-pin-button__${id}`}
24
- className="myft-pin-button"
25
- data-prioritise-button
26
- data-trackable="prioritised"
27
- data-concept-id={id}
28
- data-prioritised={prioritised ? true : false}
29
- aria-label={`${prioritised ? 'Unpin' : 'Pin'} ${name} ${prioritised ? 'from' : 'in'} myFT`}
30
- aria-pressed={prioritised ? true : false}
31
- title={`${prioritised ? 'Unpin' : 'Pin'} ${name}`}>
32
- </button>
33
- </form>
34
- </div>
35
- </Fragment>
36
- }
13
+ <span className="myft-pin-divider"></span>
14
+ <div className="myft-pin-button-wrapper">
15
+ <form method="post" action={getAction()} data-myft-prioritise>
16
+ <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
17
+ <input type="hidden" value={name} name="name" />
18
+ <input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
19
+ <div
20
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
21
+ aria-live="assertive"
22
+ data-pressed-text={`${name} pinned in myFT.`}
23
+ data-unpressed-text={`Unpinned ${name} from myFT.`}
24
+ ></div>
25
+ <button id={`myft-pin-button__${id}`}
26
+ className="myft-pin-button"
27
+ data-prioritise-button
28
+ data-trackable="prioritised"
29
+ data-concept-id={id}
30
+ data-prioritised={prioritised ? true : false}
31
+ aria-label={`${prioritised ? 'Unpin' : 'Pin'} ${name} ${prioritised ? 'from' : 'in'} my F T`}
32
+ aria-pressed={prioritised ? true : false}
33
+ title={`${prioritised ? 'Unpin' : 'Pin'} ${name}`}>
34
+ </button>
35
+ </form>
36
+ </div>
37
37
  </Fragment>
38
38
  )
39
39
 
@@ -34,7 +34,7 @@ describe('Pin Button', () => {
34
34
 
35
35
  test('It renders unprioritised', () => {
36
36
  const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
37
- expect(container.querySelector('button[aria-label="Pin myFT Enterprises in myFT"]')).toBeTruthy();
37
+ expect(container.querySelector('button[aria-label="Pin myFT Enterprises in my F T"]')).toBeTruthy();
38
38
  expect(container.querySelector('button[title="Pin myFT Enterprises"]')).toBeTruthy();
39
39
  expect(container.querySelector('button[data-prioritised=false]')).toBeTruthy();
40
40
  expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
@@ -42,7 +42,7 @@ describe('Pin Button', () => {
42
42
 
43
43
  test('It renders with prioritised', () => {
44
44
  const { container } = render(<PinButton flags={flags} prioritised={true} {...fixtures[0]} />);
45
- expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from myFT"]')).toBeTruthy();
45
+ expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from my F T"]')).toBeTruthy();
46
46
  expect(container.querySelector('button[title="Unpin myFT Enterprises"]')).toBeTruthy();
47
47
  expect(container.querySelector('button[data-prioritised=true]')).toBeTruthy();
48
48
  expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
@@ -3,6 +3,19 @@ import CsrfToken from '../csrf-token/input';
3
3
 
4
4
  const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
5
5
 
6
+ const DefaultButtonText = () => {
7
+ if (appIsStreamPage !== true) {
8
+ return <Fragment>
9
+ <span className="save-button-longer-copy" data-variant-label>
10
+ {isSaved ? 'Saved ' : 'Save '}
11
+ </span>
12
+ <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
13
+ </Fragment>
14
+ }
15
+
16
+ return <span>{isSaved ? 'Saved' : 'Save'}</span>;
17
+ }
18
+
6
19
  return (<Fragment>
7
20
  {
8
21
  saveButtonWithIcon &&
@@ -16,28 +29,13 @@ const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPag
16
29
  !saveButtonWithIcon &&
17
30
  <Fragment>
18
31
  {buttonText && buttonText}
19
- {!buttonText &&
20
- <Fragment>
21
- {
22
- appIsStreamPage !== true &&
23
- <Fragment>
24
- <span className="save-button-longer-copy" data-variant-label>
25
- {isSaved ? 'Saved ' : 'Save '}
26
- </span>
27
- <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
28
- </Fragment>
29
- }
30
-
31
- {
32
- appIsStreamPage === true && <span>{isSaved ? 'Saved' : 'Save'}</span>
33
- }
34
- </Fragment>
32
+ {!buttonText && <DefaultButtonText />
35
33
  }
36
34
  </Fragment>
37
35
  }
38
36
  </Fragment>);
39
37
  }
40
- export default function SaveForLater ({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText }) {
38
+ export default function SaveForLater({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText, csrfToken, cacheablePersonalisedUrl }) {
41
39
 
42
40
  const { myFtApiWrite } = flags;
43
41
 
@@ -51,7 +49,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
51
49
  };
52
50
 
53
51
  if (isSaved) {
54
- let titleText = `${title ? `${title} is` : ''} Saved to myFT`;
52
+ let titleText = `${title ? `${title} is` : ''} saved to myFT`;
55
53
  props['title'] = title;
56
54
  props['aria-label'] = titleText;
57
55
  props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
@@ -60,7 +58,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
60
58
  let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
61
59
  props['title'] = titleText;
62
60
  props['aria-label'] = titleText;
63
- props['data-alternate-label'] = `${title ? `${title} is` : ''} Saved to myFT`;
61
+ props['data-alternate-label'] = `${title ? `${title} is` : ''} saved to myFT`;
64
62
  props['aria-pressed'] = false;
65
63
  }
66
64
 
@@ -84,7 +82,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
84
82
  data-myft-ui="saved"
85
83
  action={`/myft/save/${contentId}`}
86
84
  data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
87
- <CsrfToken />
85
+ <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
88
86
 
89
87
  <div
90
88
  className="n-myft-ui__announcement o-normalise-visually-hidden"
@@ -93,7 +91,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
93
91
  data-unpressed-text="Removed article from My FT."
94
92
  ></div>
95
93
  <button {...generateSubmitButtonProps()}>
96
- <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage}/>
94
+ <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage} />
97
95
  </button>
98
96
  </form>
99
97
  }
@@ -83,10 +83,9 @@
83
83
  <h2 class="demo-section__title">
84
84
  Instant Alert
85
85
  </h2>
86
- {{> n-myft-ui/components/instant-alert/instant-alert }}
86
+ {{{renderReactComponent localPath="components/instant-alert/instant-alert" flags=@root.flags title=title conceptId=id name=name directType=directType }}}
87
87
  {{/instantAlert}}
88
88
 
89
-
90
89
  </div>
91
90
  </div>
92
91
  </section>