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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/.circleci/config.yml +27 -30
  2. package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
  3. package/.github/settings.yml +1 -1
  4. package/.scss-lint.yml +3 -3
  5. package/Makefile +1 -0
  6. package/README.md +62 -8
  7. package/build-state/npm-shrinkwrap.json +49383 -17508
  8. package/components/collections/collections.jsx +68 -0
  9. package/components/collections/collections.test.js +83 -0
  10. package/components/concept-list/concept-list.jsx +55 -0
  11. package/components/concept-list/concept-list.test.js +116 -0
  12. package/components/csrf-token/__tests__/input.test.js +23 -0
  13. package/components/csrf-token/input.jsx +26 -0
  14. package/components/follow-button/__tests__/follow-button.test.js +40 -0
  15. package/components/follow-button/follow-button.jsx +174 -0
  16. package/components/index.js +15 -0
  17. package/components/instant-alert/instant-alert.html +1 -1
  18. package/components/pin-button/pin-button.jsx +40 -0
  19. package/components/pin-button/pin-button.test.js +57 -0
  20. package/components/save-for-later/save-for-later.jsx +103 -0
  21. package/components/save-for-later/save-for-later.test.js +59 -0
  22. package/components/unread-articles-indicator/date-fns.js +5 -12
  23. package/demos/app.js +39 -21
  24. package/demos/templates/demo-layout.html +1 -1
  25. package/demos/templates/demo.html +436 -415
  26. package/demos/templates/demo.jsx +125 -0
  27. package/dist/bundles/bundle.js +3133 -0
  28. package/jest.config.js +8 -0
  29. package/package.json +38 -13
  30. package/webpack.config.js +34 -0
  31. package/components/collections/collections.html +0 -85
  32. package/components/concept-list/concept-list.html +0 -31
  33. package/components/csrf-token/input.html +0 -5
  34. package/components/follow-button/follow-button.html +0 -79
  35. package/components/pin-button/pin-button.html +0 -20
  36. package/components/save-for-later/save-for-later.html +0 -67
  37. package/demos/fixtures/follow-button-plus-digest.json +0 -6
  38. package/demos/templates/digest-on-follow.html +0 -12
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+ import FollowButton from '../follow-button/follow-button';
4
+
5
+ export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [] }) {
6
+ const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
7
+ let formProps = {
8
+ method: 'POST',
9
+ action: '#',
10
+ 'data-myft-ui': 'follow',
11
+ 'data-concept-id': concepts.map(concept => concept.id).join(',')
12
+ };
13
+
14
+ if (collectionName) {
15
+ formProps['data-myft-tracking'] = collectionName;
16
+ }
17
+
18
+ return (
19
+ <section
20
+ className={`collection collection--${getLiteStyleModifier()}`}
21
+ data-trackable={trackable ? trackable : 'collection'}>
22
+ <header className={`collection__header collection__header--${getLiteStyleModifier()}`}>
23
+ <h2 className={`collection__title collection__title--${getLiteStyleModifier()}`}>
24
+ {title}
25
+ </h2>
26
+ </header>
27
+ <ul className="collection__concepts">
28
+ {concepts && concepts.map((concept, index) =>
29
+ <li className="collection__concept" key={index}>
30
+ <FollowButton variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
31
+ </li>)
32
+ }
33
+ </ul>
34
+
35
+ <div className="collection__meta">
36
+ <form
37
+ {...formProps}
38
+ className="n-myft-ui n-myft-ui--follow n-ui-hide-core collection-follow-all">
39
+ <input
40
+ type="hidden"
41
+ name="directType"
42
+ value={concepts.map(concept => concept.directType).join(',')}
43
+ />
44
+ <CsrfToken />
45
+ <input
46
+ type="hidden"
47
+ name="name"
48
+ value={concepts.map(concept => concept.name).join(',')}
49
+ />
50
+ <button
51
+ type="submit"
52
+ aria-pressed="false"
53
+ className={`collection-follow-all__button collection-follow-all__button--${getLiteStyleModifier()}`}
54
+ data-trackable="follow all"
55
+ data-concept-id={concepts.map(concept => concept.id).join(',')}
56
+ aria-label={`Add all topics in the ${title} collection to my F T`}
57
+ data-alternate-label={`Remove all topics in the ${title} collection from my F T`}
58
+ data-alternate-text="Added"
59
+ title={`Add all topics in the ${title} collection to my F T`}>
60
+ Add all to myFT
61
+ </button>
62
+ </form>
63
+ </div>
64
+ </section>
65
+
66
+ )
67
+
68
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import Collections from './collections';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const fixtures = {
7
+ 'title': 'European Union',
8
+ 'concepts': [
9
+ {
10
+ 'id': '00000000-0000-0000-0000-000000000000',
11
+ 'prefLabel': 'EU immigration',
12
+ 'directType': 'http://www.ft.com/ontology/Topic',
13
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
14
+ 'name': 'EU immigration'
15
+ },
16
+ {
17
+ 'id': '00000000-0000-0000-0000-000000000001',
18
+ 'prefLabel': 'Europe Quantitative Easing',
19
+ 'directType': 'http://www.ft.com/ontology/Topic',
20
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
21
+ 'name': 'Europe Quantitative Easing'
22
+ },
23
+ {
24
+ 'id': '00000000-0000-0000-0000-000000000002',
25
+ 'prefLabel': 'EU financial regulation',
26
+ 'directType': 'http://www.ft.com/ontology/Topic',
27
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
28
+ 'name': 'EU financial regulation'
29
+ },
30
+ {
31
+ 'id': '00000000-0000-0000-0000-000000000003',
32
+ 'prefLabel': 'EU nothing',
33
+ 'directType': 'http://www.ft.com/ontology/Topic',
34
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
35
+ 'name': 'EU nothing'
36
+ },
37
+ {
38
+ 'id': '00000000-0000-0000-0000-000000000004',
39
+ 'prefLabel': 'EU trade',
40
+ 'directType': 'http://www.ft.com/ontology/Topic',
41
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
42
+ 'name': 'EU trade'
43
+ }
44
+ ]
45
+ };
46
+ const joinedDirectTypes = 'http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic';
47
+
48
+ const flags = {
49
+ myFtApi: true,
50
+ myFtApiWrite: true
51
+ };
52
+
53
+ describe('Concept List', () => {
54
+
55
+ test('It renders the title of the collection', async () => {
56
+ render(<Collections {...fixtures} flags={flags} />);
57
+ expect(await screen.findByText('European Union')).toBeTruthy();
58
+ });
59
+
60
+ test('It renders label for the concept button', async () => {
61
+ render(<Collections {...fixtures} flags={flags} />);
62
+ expect(await screen.findByText('EU immigration')).toBeTruthy();
63
+ expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
64
+ expect(await screen.findByText('EU financial regulation')).toBeTruthy();
65
+ expect(await screen.findByText('EU nothing')).toBeTruthy();
66
+ expect(await screen.findByText('EU trade')).toBeTruthy();
67
+ });
68
+
69
+ test('It renders form "Add all to my FT" from', () => {
70
+ const { container} = render(<Collections {...fixtures} flags={flags} />);
71
+ const formElement = container.querySelector('form[action="#"]');
72
+ expect(formElement).toBeTruthy();
73
+ expect(formElement.method).toEqual('post');
74
+ });
75
+
76
+ test('It renders directType input with value of types joined', () => {
77
+ const { container} = render(<Collections {...fixtures} flags={flags} />);
78
+ const directTypeElement = container.querySelector('input[name="directType"]');
79
+ expect(directTypeElement).toBeTruthy();
80
+ expect(directTypeElement.value).toEqual(joinedDirectTypes);
81
+ });
82
+
83
+ });
@@ -0,0 +1,55 @@
1
+ import React, { Fragment } from 'react';
2
+ import FollowButton from '../follow-button/follow-button';
3
+
4
+ export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable }) {
5
+
6
+ const {
7
+ myFtApi,
8
+ myFtApiWrite
9
+ } = flags;
10
+
11
+ const generateTrackableProps = (primary, seconday) => {
12
+ return {
13
+ 'data-trackable': primary ? primary : seconday
14
+ }
15
+ }
16
+
17
+
18
+ return (
19
+
20
+ <Fragment>
21
+ {(myFtApi && myFtApiWrite && concepts && concepts.length) &&
22
+ <div
23
+ className='concept-list'
24
+ {...generateTrackableProps(trackable, 'concept-list')}>
25
+ {
26
+ (contentType || conceptListTitle) &&
27
+ <h2 className='concept-list__title'>
28
+ {conceptListTitle ? conceptListTitle : `Follow the topics in this ${contentType}`}
29
+ </h2>
30
+ }
31
+ <ul className='concept-list__list'>
32
+ {concepts.map((concept, index) => {
33
+ const {
34
+ relativeUrl,
35
+ url,
36
+ conceptTrackable,
37
+ prefLabel,
38
+ id
39
+ } = concept;
40
+ return (
41
+ <li key={index} className='concept-list__list-item'>
42
+ <a
43
+ href={relativeUrl || url}
44
+ {...generateTrackableProps(conceptTrackable, 'concept')}
45
+ className='concept-list__concept'>
46
+ {prefLabel}
47
+ </a>
48
+ <FollowButton conceptId={id} name={prefLabel} flags={flags} />
49
+ </li>
50
+ )
51
+ })}
52
+ </ul>
53
+ </div>}
54
+ </Fragment>)
55
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import ConceptList from './concept-list';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const fixtures = [
7
+ {
8
+ 'conceptListTitle': 'Follow european union things',
9
+ 'concepts': [
10
+ {
11
+ 'id': '00000000-0000-0000-0000-000000000161',
12
+ 'prefLabel': 'EU immigration',
13
+ 'directType': 'http://www.ft.com/ontology/Topic',
14
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000161',
15
+ 'name': 'EU immigration'
16
+ },
17
+ {
18
+ 'id': '00000000-0000-0000-0000-000000000162',
19
+ 'prefLabel': 'Europe Quantitative Easing',
20
+ 'directType': 'http://www.ft.com/ontology/Topic',
21
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000162',
22
+ 'name': 'Europe Quantitative Easing'
23
+ },
24
+ {
25
+ 'id': '00000000-0000-0000-0000-000000000163',
26
+ 'prefLabel': 'EU financial regulation',
27
+ 'directType': 'http://www.ft.com/ontology/Topic',
28
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000163',
29
+ 'name': 'EU financial regulation'
30
+ },
31
+ {
32
+ 'id': '00000000-0000-0000-0000-000000000164',
33
+ 'prefLabel': 'EU nothing',
34
+ 'directType': 'http://www.ft.com/ontology/Topic',
35
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000164',
36
+ 'name': 'EU nothing'
37
+ },
38
+ {
39
+ 'id': '00000000-0000-0000-0000-000000000165',
40
+ 'prefLabel': 'EU trade',
41
+ 'directType': 'http://www.ft.com/ontology/Topic',
42
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000165',
43
+ 'name': 'EU trade'
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ 'contentType': 'search',
49
+ 'concepts': [
50
+ {
51
+ 'id': '00000000-0000-0000-0000-000000000166',
52
+ 'prefLabel': 'Noodle',
53
+ 'directType': 'http://www.ft.com/ontology/Topic',
54
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000166',
55
+ 'name': 'Noodle'
56
+ },
57
+ {
58
+ 'id': '00000000-0000-0000-0000-000000000167',
59
+ 'prefLabel': 'Green apples',
60
+ 'directType': 'http://www.ft.com/ontology/Topic',
61
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000167',
62
+ 'name': 'Green apples'
63
+ },
64
+ {
65
+ 'id': '00000000-0000-0000-0000-000000000168',
66
+ 'prefLabel': 'Fox blood',
67
+ 'directType': 'http://www.ft.com/ontology/Topic',
68
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000168',
69
+ 'name': 'Fox blood'
70
+ },
71
+ {
72
+ 'id': '00000000-0000-0000-0000-000000000169',
73
+ 'prefLabel': 'Dog party',
74
+ 'directType': 'http://www.ft.com/ontology/Topic',
75
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000169',
76
+ 'name': 'Dog party'
77
+ },
78
+ {
79
+ 'id': '00000000-0000-0000-0000-000000000170',
80
+ 'prefLabel': 'Fifth thing',
81
+ 'directType': 'http://www.ft.com/ontology/Topic',
82
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000170',
83
+ 'name': 'Fifth thing'
84
+ }
85
+ ]
86
+ },
87
+ ];
88
+
89
+
90
+ const flags = {
91
+ myFtApi: true,
92
+ myFtApiWrite: true
93
+ };
94
+
95
+ describe('Concept List', () => {
96
+
97
+ test('It renders conceptListTitle value as title when conceptListTitle is provided', async () => {
98
+ render(<ConceptList {...fixtures[0]} flags={flags} />);
99
+ expect(await screen.findByText('Follow european union things')).toBeTruthy();
100
+ });
101
+
102
+ test('It renders "Follow the topics in this {conceptType}" value as title when conceptType is provided', async () => {
103
+ render(<ConceptList {...fixtures[1]} flags={flags} />);
104
+ expect(await screen.findByText('Follow the topics in this search')).toBeTruthy();
105
+ });
106
+
107
+ test('It renders label for the concept button', async () => {
108
+ render(<ConceptList {...fixtures[0]} flags={flags} />);
109
+ expect(await screen.findByText('EU immigration')).toBeTruthy();
110
+ expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
111
+ expect(await screen.findByText('EU financial regulation')).toBeTruthy();
112
+ expect(await screen.findByText('EU nothing')).toBeTruthy();
113
+ expect(await screen.findByText('EU trade')).toBeTruthy();
114
+ });
115
+
116
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import CsrfToken from '../input';
3
+ import { render } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const props = {
7
+ cacheablePersonalisedUrl: false
8
+ };
9
+
10
+ describe('Csrf Token Input', () => {
11
+
12
+ test('It renders default button', async () => {
13
+ let { container } = render(<CsrfToken {...props} />);
14
+ expect(container.querySelector('[name=\'token\']')).toBeTruthy();
15
+ });
16
+
17
+ test('It renders csrf token attribute', async () => {
18
+ let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
19
+ expect(container.querySelector('[data-myft-csrf-token=\'test-token\']')).toBeTruthy();
20
+ });
21
+
22
+
23
+ });
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
4
+
5
+ let inputProps = {};
6
+
7
+ if (cacheablePersonalisedUrl) {
8
+ inputProps = {
9
+ ...inputProps,
10
+ 'data-myft-csrf-token': csrfToken
11
+ };
12
+ }
13
+
14
+ if(csrfToken) {
15
+ inputProps.value = csrfToken;
16
+ }
17
+
18
+ return (
19
+ <input
20
+ {...inputProps}
21
+ type="hidden"
22
+ name="token"
23
+ />
24
+ );
25
+
26
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import FollowButton from '../follow-button';
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: 'Follow button'
13
+ };
14
+
15
+ describe('Follow button', () => {
16
+
17
+ test('It renders default button', async () => {
18
+ render(<FollowButton {...props} />);
19
+ expect(await screen.findByText('Add to myFT')).toBeTruthy();
20
+ });
21
+
22
+ test('It renders a variant', async () => {
23
+ const { container } = render(<FollowButton {...props} variant={'standard'} />);
24
+ expect(container.getElementsByClassName('n-myft-follow-button--standard')).toHaveLength(1);
25
+ });
26
+
27
+ test('It renders follow button form', async () => {
28
+ const { container } = render(<FollowButton {...props} variant={'standard'} />);
29
+ expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
30
+ });
31
+
32
+ test('Button state changes when attributes change', async () => {
33
+ render(<FollowButton {...props}
34
+ variant={'standard'}
35
+ setFollowButtonStateToSelected={true}
36
+ cacheablePersonalisedUrl={true} />);
37
+ expect(await screen.findByText('Added')).toBeTruthy();
38
+ });
39
+
40
+ });
@@ -0,0 +1,174 @@
1
+ import React, {Fragment} from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ function generateFormProps (props) {
5
+ let generatedProps = {};
6
+
7
+ const {
8
+ collectionName,
9
+ followPlusDigestEmail,
10
+ conceptId,
11
+ setFollowButtonStateToSelected,
12
+ cacheablePersonalisedUrl
13
+ } = props;
14
+
15
+ if (collectionName) {
16
+ generatedProps['data-myft-tracking'] = `collectionName=${collectionName}`;
17
+ }
18
+
19
+ if(followPlusDigestEmail) {
20
+ generatedProps['action'] = `/__myft/api/core/follow-plus-digest-email/${conceptId}?method=put`;
21
+ generatedProps['data-myft-ui-variant'] = 'followPlusDigestEmail';
22
+ } else {
23
+ if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
24
+ generatedProps['action'] = `/myft/remove/${conceptId}`;
25
+ generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=delete`;
26
+ } else {
27
+ generatedProps['action'] = `/myft/add/${conceptId}`;
28
+ generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=put`;
29
+ }
30
+ }
31
+
32
+ return generatedProps;
33
+
34
+ }
35
+
36
+ function generateButtonProps (props) {
37
+
38
+ const {
39
+ cacheablePersonalisedUrl,
40
+ setFollowButtonStateToSelected,
41
+ name,
42
+ buttonText,
43
+ variant,
44
+ conceptId,
45
+ alternateText,
46
+ followPlusDigestEmail
47
+ } = props;
48
+
49
+ let generatedProps = {
50
+ 'data-concept-id': conceptId,
51
+ 'n-myft-follow-button': 'true',
52
+ 'data-trackable': 'follow',
53
+ type: 'submit'
54
+ };
55
+
56
+ if (cacheablePersonalisedUrl && setFollowButtonStateToSelected) {
57
+ generatedProps['aria-label'] = `Remove ${name} from myFT`;
58
+ generatedProps['title'] = `Remove ${name} from myFT`
59
+ generatedProps['data-alternate-label'] = `Add ${name} to myFT`;
60
+ generatedProps['aria-pressed'] = true;
61
+
62
+ if(alternateText) {
63
+ generatedProps['data-alternate-text'] = alternateText;
64
+ } else {
65
+ if(buttonText) {
66
+ generatedProps['data-alternate-text'] = buttonText;
67
+ } else {
68
+ generatedProps['data-alternate-text'] = 'Add to myFT';
69
+ }
70
+ }
71
+ } else {
72
+ generatedProps['aria-pressed'] = false;
73
+ generatedProps['aria-label'] = `Add ${name} to myFT`;
74
+ generatedProps['title'] = `Add ${name} to myFT`;
75
+ generatedProps['data-alternate-label'] = `Remove ${name} from myFT`;
76
+ if (alternateText) {
77
+ generatedProps['data-alternate-text'] = alternateText;
78
+ } else {
79
+ if (buttonText) {
80
+ generatedProps['data-alternate-text'] = buttonText;
81
+ } else {
82
+ generatedProps['data-alternate-text'] = 'Added';
83
+ }
84
+ }
85
+ }
86
+
87
+ if(variant) {
88
+ generatedProps[`n-myft-follow-button--${variant}`] = 'true';
89
+ }
90
+
91
+ if(followPlusDigestEmail) {
92
+ generatedProps['data-trackable-context-messaging'] = 'add-to-myft-plus-digest-button';
93
+ }
94
+
95
+ return generatedProps;
96
+ }
97
+
98
+ function getButtonText (props) {
99
+
100
+ const {
101
+ buttonText,
102
+ setFollowButtonStateToSelected,
103
+ cacheablePersonalisedUrl
104
+ } = props;
105
+ let outputText;
106
+
107
+ if(buttonText) {
108
+ outputText = buttonText;
109
+ } else {
110
+ if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
111
+ outputText = 'Added';
112
+ } else {
113
+ outputText = 'Add to myFT';
114
+ }
115
+ }
116
+
117
+ return outputText;
118
+ }
119
+
120
+ /**
121
+ *
122
+ * @param {Object} props
123
+ * @param {string} props.name
124
+ * @param {Object} props.flags
125
+ * @param {string} props.extraClasses
126
+ * @param {string} props.conceptId
127
+ * @param {string} props.variant
128
+ * @param {string} props.buttonText
129
+ * @param {*} props.setFollowButtonStateToSelected
130
+ * @param {string} props.cacheablePersonalisedUrl
131
+ * @param {string} props.alternateText
132
+ * @param {*} props.followPlusDigestEmail
133
+ * @param {string} props.collectionName
134
+ */
135
+ export default function FollowButton (props) {
136
+
137
+ const {
138
+ name,
139
+ flags,
140
+ extraClasses,
141
+ conceptId,
142
+ variant,
143
+ } = props;
144
+
145
+ const formProps = generateFormProps(props);
146
+ const buttonProps = generateButtonProps(props);
147
+
148
+ const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
149
+
150
+ return (
151
+ <Fragment>
152
+ {flags.myFtApiWrite && <form
153
+ className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
154
+ method="GET"
155
+ data-myft-ui="follow"
156
+ data-concept-id={conceptId}
157
+ {...formProps}>
158
+ <CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
159
+ <div
160
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
161
+ aria-live="assertive"
162
+ data-pressed-text={`Now following ${name}.`}
163
+ data-unpressed-text={`No longer following ${name}.`}
164
+ ></div>
165
+ <button
166
+ {...buttonProps}
167
+ className={[`n-myft-follow-button ${getVariantClass(variant)}`]}>
168
+ {getButtonText(props)}
169
+ </button>
170
+ </form>}
171
+ </Fragment>
172
+ );
173
+
174
+ }
@@ -0,0 +1,15 @@
1
+ import CsrfToken from './csrf-token/input';
2
+ import FollowButton from './follow-button/follow-button';
3
+ import ConceptList from './concept-list/concept-list';
4
+ import Collections from './collections/collections';
5
+ import SaveForLater from './save-for-later/save-for-later';
6
+ import PinButton from './pin-button/pin-button';
7
+
8
+ export {
9
+ CsrfToken,
10
+ FollowButton,
11
+ ConceptList,
12
+ Collections,
13
+ SaveForLater,
14
+ PinButton
15
+ };
@@ -5,7 +5,7 @@
5
5
  data-concept-id="{{conceptId}}"
6
6
  action="/myft/add/{{conceptId}}?instant=true"
7
7
  data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put">
8
- {{> n-myft-ui/components/csrf-token/input}}
8
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
9
9
  <input type="hidden" value="{{name}}" name="name">
10
10
  {{#if directType}}
11
11
  <input type="hidden" value="{{directType}}" name="directType">
@@ -0,0 +1,40 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+ export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised }) {
4
+
5
+ const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`
6
+
7
+ return (
8
+ <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
+ }
37
+ </Fragment>
38
+ )
39
+
40
+ }