@financial-times/n-myft-ui 23.0.1 → 24.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +13 -16
- package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
- package/.github/settings.yml +1 -1
- package/.scss-lint.yml +3 -3
- package/Makefile +1 -0
- package/README.md +11 -8
- package/build-state/npm-shrinkwrap.json +11383 -10733
- package/components/collections/collections.html +3 -11
- package/components/concept-list/concept-list.html +1 -4
- package/components/csrf-token/__tests__/input.test.js +23 -0
- package/components/csrf-token/input.jsx +26 -0
- package/components/follow-button/__tests__/follow-button.test.js +40 -0
- package/components/follow-button/follow-button.jsx +174 -0
- package/components/instant-alert/instant-alert.html +1 -1
- package/components/pin-button/pin-button.html +1 -1
- package/components/save-for-later/save-for-later.html +6 -9
- package/components/unread-articles-indicator/README.md +2 -62
- package/components/unread-articles-indicator/date-fns.js +5 -12
- package/components/unread-articles-indicator/index.js +1 -62
- package/components/unread-articles-indicator/storage.js +0 -44
- package/demos/app.js +30 -3
- package/demos/templates/demo.html +2 -4
- package/demos/templates/demo.jsx +33 -0
- package/demos/templates/digest-on-follow.html +1 -1
- package/jest.config.js +8 -0
- package/mixins/lozenge/_themes.scss +8 -4
- package/mixins/lozenge/main.scss +50 -4
- package/myft/main.scss +0 -1
- package/myft/ui/index.js +0 -2
- package/package.json +20 -6
- package/test/unread-articles-indicator/index.spec.js +13 -65
- package/test/unread-articles-indicator/storage.spec.js +0 -93
- package/components/csrf-token/input.html +0 -5
- package/components/follow-button/follow-button.html +0 -79
- package/components/header-tooltip/index.js +0 -37
- package/components/header-tooltip/main.scss +0 -12
- package/components/unread-articles-indicator/constants.js +0 -4
- package/components/unread-articles-indicator/count-unread-articles.js +0 -26
- package/components/unread-articles-indicator/main.scss +0 -59
- package/components/unread-articles-indicator/tracking.js +0 -15
- package/components/unread-articles-indicator/ui.js +0 -99
- package/components/unread-articles-indicator/update-count.js +0 -40
- package/test/unread-articles-indicator/count-unread-articles.spec.js +0 -72
- package/test/unread-articles-indicator/tracking.spec.js +0 -26
- package/test/unread-articles-indicator/ui.spec.js +0 -123
- package/test/unread-articles-indicator/update-count.spec.js +0 -156
@@ -12,7 +12,7 @@
|
|
12
12
|
</h2>
|
13
13
|
|
14
14
|
{{#followButton}}
|
15
|
-
{{
|
15
|
+
{{{renderReactComponent localPath="components/follow-button/follow-button" flags=@root.flags}}}
|
16
16
|
{{/followButton}}
|
17
17
|
|
18
18
|
<h2
|
@@ -21,9 +21,7 @@
|
|
21
21
|
</h2>
|
22
22
|
|
23
23
|
{{#followButton}}
|
24
|
-
{{
|
25
|
-
buttonText=name
|
26
|
-
}}
|
24
|
+
{{{renderReactComponent localPath="components/follow-button/follow-button" buttonText=name flags=@root.flags}}}
|
27
25
|
{{/followButton}}
|
28
26
|
|
29
27
|
{{#saveButton}}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import FollowButton from '../../components/follow-button/follow-button';
|
3
|
+
|
4
|
+
export default function Demo (props) {
|
5
|
+
|
6
|
+
const {
|
7
|
+
title,
|
8
|
+
flags,
|
9
|
+
followButton,
|
10
|
+
} = props;
|
11
|
+
|
12
|
+
const followButtonProps = {...followButton, flags};
|
13
|
+
|
14
|
+
return (
|
15
|
+
<div className="o-grid-container o-grid-container--snappy demo-container">
|
16
|
+
<h1>{title}</h1>
|
17
|
+
|
18
|
+
<section
|
19
|
+
id="follow-button"
|
20
|
+
className="demo-section">
|
21
|
+
<div className="o-grid-row">
|
22
|
+
<div data-o-grid-colspan="12">
|
23
|
+
<h2
|
24
|
+
className="demo-section__title">
|
25
|
+
Follow button
|
26
|
+
</h2>
|
27
|
+
<FollowButton {...followButtonProps} />
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</section>
|
31
|
+
</div>
|
32
|
+
)
|
33
|
+
}
|
package/jest.config.js
ADDED
@@ -4,28 +4,32 @@ $myft-lozenge-themes: (
|
|
4
4
|
text: oColorsByName('white'),
|
5
5
|
highlight: oColorsByName('claret-50'),
|
6
6
|
pressed-highlight: rgba(oColorsByName('black'), 0.05),
|
7
|
-
disabled: rgba(oColorsByName('black'), 0.5)
|
7
|
+
disabled: rgba(oColorsByName('black'), 0.5),
|
8
|
+
focus-outline: oColorsByUsecase('focus', 'outline', $fallback: null)
|
8
9
|
),
|
9
10
|
inverse: (
|
10
11
|
background: oColorsByName('white'),
|
11
12
|
text: oColorsByName('claret'),
|
12
13
|
highlight: rgba(white, 0.8),
|
13
14
|
pressed-highlight: rgba(white, 0.2),
|
14
|
-
disabled: rgba(oColorsByName('white'), 0.5)
|
15
|
+
disabled: rgba(oColorsByName('white'), 0.5),
|
16
|
+
focus-outline: oColorsByName('white')
|
15
17
|
),
|
16
18
|
opinion: (
|
17
19
|
background: oColorsByName('oxford-40'),
|
18
20
|
text: oColorsByName('white'),
|
19
21
|
highlight: oColorsByName('oxford-30'),
|
20
22
|
pressed-highlight: rgba(oColorsByName('oxford-40'), 0.2),
|
21
|
-
disabled: rgba(oColorsByName('black'), 0.5)
|
23
|
+
disabled: rgba(oColorsByName('black'), 0.5),
|
24
|
+
focus-outline: oColorsByUsecase('focus', 'outline', $fallback: null)
|
22
25
|
),
|
23
26
|
monochrome: (
|
24
27
|
background: oColorsByName('white'),
|
25
28
|
text: oColorsByName('black'),
|
26
29
|
highlight: oColorsByName('white-80'),
|
27
30
|
pressed-highlight: rgba(oColorsByName('white'), 0.2),
|
28
|
-
disabled: rgba(oColorsByName('white'), 0.5)
|
31
|
+
disabled: rgba(oColorsByName('white'), 0.5),
|
32
|
+
focus-outline: oColorsByName('white')
|
29
33
|
)
|
30
34
|
);
|
31
35
|
|
package/mixins/lozenge/main.scss
CHANGED
@@ -1,6 +1,54 @@
|
|
1
1
|
@import './themes';
|
2
2
|
@import './toggle-icon';
|
3
3
|
|
4
|
+
@mixin focusOutlineColor($focus-color) {
|
5
|
+
// Apply :focus styles as a fallback
|
6
|
+
// These styles will be applied to all browsers that don't use the polyfill, this includes browsers which support the feature natively.
|
7
|
+
body:not(.js-focus-visible) &,
|
8
|
+
html:not(.js-focus-visible) & {
|
9
|
+
// Standardise focus styles.
|
10
|
+
&:focus {
|
11
|
+
outline: 2px solid $focus-color;
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
// When the focus-visible polyfill is applied `.js-focus-visible` is added to the html dom node
|
16
|
+
// (the body node in v4 of the 3rd party polyfill)
|
17
|
+
|
18
|
+
// stylelint-disable-next-line selector-no-qualifying-type
|
19
|
+
body.js-focus-visible &, // stylelint-disable-next-line selector-no-qualifying-type
|
20
|
+
html.js-focus-visible & {
|
21
|
+
// Standardise focus styles.
|
22
|
+
// stylelint-disable-next-line selector-no-qualifying-type
|
23
|
+
&.focus-visible {
|
24
|
+
outline: 2px solid $focus-color;
|
25
|
+
}
|
26
|
+
// Disable browser default focus style.
|
27
|
+
// stylelint-disable-next-line selector-no-qualifying-type
|
28
|
+
&:focus:not(.focus-visible) {
|
29
|
+
outline: 0;
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
// These styles will be ignored by browsers which do not recognise the :focus-visible selector (as per the third bullet point in https://www.w3.org/TR/selectors-3/#Conformance)
|
34
|
+
// If a browser supports :focus-visible we unset the :focus styles that were applied above
|
35
|
+
// (within the html:not(.js-focus-visible) block).
|
36
|
+
&:focus-visible,
|
37
|
+
body:not(.js-focus-visible) &:focus,
|
38
|
+
html:not(.js-focus-visible) &:focus {
|
39
|
+
outline: unset;
|
40
|
+
}
|
41
|
+
|
42
|
+
// Styles given :focus-visible support. Extra selectors needed to match
|
43
|
+
// previous `:focus` selector specificity.
|
44
|
+
body:not(.js-focus-visible) &:focus-visible,
|
45
|
+
html:not(.js-focus-visible) &:focus-visible,
|
46
|
+
&:focus-visible {
|
47
|
+
outline: 2px solid $focus-color;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
|
4
52
|
@mixin myftLozengeTheme($theme: standard, $with-toggle-icon: false) {
|
5
53
|
@if $with-toggle-icon != false {
|
6
54
|
@include myftToggleIcon($theme);
|
@@ -11,6 +59,8 @@
|
|
11
59
|
border: 1px solid getThemeColor(background);
|
12
60
|
color: getThemeColor(background);
|
13
61
|
|
62
|
+
@include focusOutlineColor(getThemeColor(focus-outline));
|
63
|
+
|
14
64
|
&:hover,
|
15
65
|
&:focus {
|
16
66
|
background-color: getThemeColor(pressed-highlight);
|
@@ -57,8 +107,4 @@
|
|
57
107
|
text-overflow: ellipsis;
|
58
108
|
transition: border-color, background-color 0.5s ease;
|
59
109
|
white-space: nowrap;
|
60
|
-
|
61
|
-
&:focus {
|
62
|
-
outline: 2px solid oColorsByName('teal-90');
|
63
|
-
}
|
64
110
|
}
|
package/myft/main.scss
CHANGED
package/myft/ui/index.js
CHANGED
@@ -2,12 +2,10 @@ import * as myFtButtons from './myft-buttons';
|
|
2
2
|
import * as lists from './lists';
|
3
3
|
import personaliseLinks from './personalise-links';
|
4
4
|
import updateUi from './update-ui';
|
5
|
-
import * as headerTooltip from '../../components/header-tooltip';
|
6
5
|
|
7
6
|
function init (opts) {
|
8
7
|
myFtButtons.init(opts);
|
9
8
|
lists.init();
|
10
|
-
headerTooltip.init();
|
11
9
|
}
|
12
10
|
|
13
11
|
export {
|
package/package.json
CHANGED
@@ -1,14 +1,11 @@
|
|
1
1
|
{
|
2
2
|
"name": "@financial-times/n-myft-ui",
|
3
|
-
"version": "
|
3
|
+
"version": "24.0.0",
|
4
4
|
"description": "Client side component for interaction with myft",
|
5
5
|
"main": "server.js",
|
6
6
|
"scripts": {
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
8
8
|
"commit": "commit-wizard",
|
9
|
-
"precommit": "node_modules/.bin/secret-squirrel",
|
10
|
-
"prepush": "make verify -j3",
|
11
|
-
"commitmsg": "node_modules/.bin/secret-squirrel-commitmsg",
|
12
9
|
"prepare": "npx snyk protect || npx snyk protect -d || true"
|
13
10
|
},
|
14
11
|
"repository": {
|
@@ -27,10 +24,15 @@
|
|
27
24
|
"@financial-times/dotcom-build-js": "0.4.1",
|
28
25
|
"@financial-times/dotcom-build-sass": "0.4.1",
|
29
26
|
"@financial-times/dotcom-page-kit-cli": "0.4.1",
|
30
|
-
"@financial-times/
|
27
|
+
"@financial-times/dotcom-server-handlebars": "^3.0.0",
|
28
|
+
"@financial-times/dotcom-server-react-jsx": "^2.6.2",
|
29
|
+
"@financial-times/n-gage": "^8.3.0",
|
31
30
|
"@financial-times/n-heroku-tools": "8.3.1",
|
32
31
|
"@financial-times/n-internal-tool": "2.3.4",
|
33
32
|
"@financial-times/x-handlebars": "1.0.0-beta.21",
|
33
|
+
"@sucrase/jest-plugin": "^2.2.0",
|
34
|
+
"@testing-library/jest-dom": "^5.16.1",
|
35
|
+
"@testing-library/react": "^12.1.2",
|
34
36
|
"ascii-table": "0.0.9",
|
35
37
|
"autoprefixer": "9.7.0",
|
36
38
|
"aws-sdk-mock": "4.5.0",
|
@@ -53,6 +55,7 @@
|
|
53
55
|
"css-loader": "^0.23.1",
|
54
56
|
"denodeify": "^1.2.1",
|
55
57
|
"eslint": "6.5.1",
|
58
|
+
"eslint-plugin-react": "^7.27.1",
|
56
59
|
"extract-css-block-webpack-plugin": "^1.3.0",
|
57
60
|
"extract-text-webpack-plugin": "3.0.2",
|
58
61
|
"fetch-mock": "^5.0.3",
|
@@ -62,6 +65,8 @@
|
|
62
65
|
"hyperons": "^0.4.1",
|
63
66
|
"imports-loader": "0.8.0",
|
64
67
|
"inject-loader": "^3.0.0",
|
68
|
+
"jest": "^27.4.5",
|
69
|
+
"jsdom": "^19.0.0",
|
65
70
|
"karma": "4.4.1",
|
66
71
|
"karma-browserstack-launcher": "1.5.1",
|
67
72
|
"karma-chai": "^0.1.0",
|
@@ -83,16 +88,25 @@
|
|
83
88
|
"npm-prepublish": "^1.2.1",
|
84
89
|
"pa11y-ci": "^2.1.1",
|
85
90
|
"postcss-loader": "^0.9.1",
|
91
|
+
"react": "^17.0.2",
|
86
92
|
"regenerator-runtime": "^0.13.3",
|
87
93
|
"sass-loader": "^3.2.0",
|
88
94
|
"semver": "6.3.0",
|
89
95
|
"sinon": "^7.1.0",
|
90
96
|
"sinon-chai": "^3.2.0",
|
91
|
-
"snyk": "^1.216.5"
|
97
|
+
"snyk": "^1.216.5",
|
98
|
+
"sucrase": "^3.10.1"
|
92
99
|
},
|
93
100
|
"x-dash": {
|
94
101
|
"engine": {
|
95
102
|
"server": "hyperons"
|
96
103
|
}
|
104
|
+
},
|
105
|
+
"husky": {
|
106
|
+
"hooks": {
|
107
|
+
"commit-msg": "node_modules/.bin/secret-squirrel-commitmsg",
|
108
|
+
"pre-commit": "node_modules/.bin/secret-squirrel",
|
109
|
+
"pre-push": "make verify -j3"
|
110
|
+
}
|
97
111
|
}
|
98
112
|
}
|
@@ -1,95 +1,43 @@
|
|
1
1
|
/* global expect */
|
2
2
|
import sinon from 'sinon';
|
3
3
|
|
4
|
-
const NEW_ARTICLES = [
|
5
|
-
{ id: 'article-1' },
|
6
|
-
{ id: 'article-2' },
|
7
|
-
{ id: 'article-3' },
|
8
|
-
];
|
9
4
|
const FEED_START_TIME = '2018-06-05T10:00:00.000Z';
|
10
5
|
|
11
6
|
describe('unread stories indicator', () => {
|
12
|
-
let
|
13
|
-
let mockUpdate;
|
7
|
+
let unreadArticlesComponent;
|
14
8
|
let mockInitialiseFeedStartTime;
|
15
9
|
let mockStorage;
|
16
|
-
let mockUi;
|
17
10
|
let isStorageAvailable;
|
18
11
|
|
19
12
|
beforeEach(() => {
|
20
13
|
mockStorage = {
|
21
14
|
getFeedStartTime: sinon.stub().returns(FEED_START_TIME),
|
22
15
|
setFeedStartTime: sinon.stub(),
|
23
|
-
|
24
|
-
updateLastUpdate: sinon.stub(),
|
25
|
-
isAvailable: sinon.stub().callsFake(() => isStorageAvailable),
|
26
|
-
setIndicatorDismissedTime: sinon.stub(),
|
27
|
-
addCountChangeListeners: sinon.stub()
|
16
|
+
isAvailable: sinon.stub().callsFake(() => isStorageAvailable)
|
28
17
|
};
|
29
|
-
|
30
|
-
createIndicators: sinon.stub(),
|
31
|
-
setCount: sinon.stub(),
|
32
|
-
getState: sinon.stub(),
|
33
|
-
};
|
34
|
-
mockUpdate = sinon.stub().returns(Promise.resolve(NEW_ARTICLES));
|
18
|
+
|
35
19
|
mockInitialiseFeedStartTime = sinon.stub().resolves();
|
36
20
|
|
37
|
-
|
21
|
+
unreadArticlesComponent = require('inject-loader!../../components/unread-articles-indicator')({
|
38
22
|
'next-session-client': {
|
39
23
|
uuid: sinon.stub().resolves({uuid: '00000000-0000-0000-0000-000000000000'})
|
40
24
|
},
|
41
25
|
'./storage': mockStorage,
|
42
|
-
'./ui': mockUi,
|
43
|
-
'./update-count': mockUpdate,
|
44
26
|
'./initialise-feed-start-time': mockInitialiseFeedStartTime
|
45
27
|
});
|
46
28
|
});
|
47
29
|
|
48
|
-
describe('
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
expect(mockUi.createIndicators).to.not.have.been.called;
|
54
|
-
expect(mockUi.setCount).to.not.have.been.called;
|
55
|
-
expect(mockInitialiseFeedStartTime).to.not.have.been.called;
|
56
|
-
});
|
30
|
+
describe('getNewsArticlesSinceTime', () => {
|
31
|
+
it('should not do anything if storage is not available', () => {
|
32
|
+
isStorageAvailable = false;
|
33
|
+
unreadArticlesComponent.getNewArticlesSinceTime();
|
34
|
+
expect(mockInitialiseFeedStartTime).to.not.have.been.called;
|
57
35
|
});
|
58
36
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
});
|
64
|
-
|
65
|
-
it('should initialise feed start time', () => {
|
66
|
-
expect(mockInitialiseFeedStartTime).to.have.been.calledOnce;
|
67
|
-
});
|
68
|
-
|
69
|
-
it('should create ui indicators', () => {
|
70
|
-
expect(mockUi.createIndicators).to.have.been.calledOnce;
|
71
|
-
});
|
72
|
-
|
73
|
-
it('should set the ui indicators initial count', () => {
|
74
|
-
expect(mockUi.setCount).to.have.been.calledOnce;
|
75
|
-
expect(mockUi.setCount).to.have.been.calledWith(22);
|
76
|
-
});
|
77
|
-
|
78
|
-
it('should set a listener for storage changes', () => {
|
79
|
-
expect(mockStorage.addCountChangeListeners).to.have.been.calledOnce;
|
80
|
-
});
|
81
|
-
|
82
|
-
|
83
|
-
it('should handle clicks', () => {
|
84
|
-
const args = mockUi.createIndicators.firstCall.args;
|
85
|
-
|
86
|
-
expect(args[1].onClick).to.be.a('function');
|
87
|
-
|
88
|
-
args[1].onClick();
|
89
|
-
|
90
|
-
expect(mockStorage.setIndicatorDismissedTime).to.have.been.calledOnce;
|
91
|
-
expect(mockStorage.updateLastUpdate).to.have.been.calledOnce;
|
92
|
-
});
|
37
|
+
it('should initialise feed start time', () => {
|
38
|
+
isStorageAvailable = true;
|
39
|
+
return unreadArticlesComponent.getNewArticlesSinceTime();
|
40
|
+
expect(mockInitialiseFeedStartTime).to.have.been.calledOnce;
|
93
41
|
});
|
94
42
|
});
|
95
43
|
});
|
@@ -126,99 +126,6 @@ describe('storage', () => {
|
|
126
126
|
});
|
127
127
|
});
|
128
128
|
|
129
|
-
describe('getLastUpdate', () => {
|
130
|
-
beforeEach(() => {
|
131
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo:1, time: now.toISOString(), updateStarted: now.toISOString()});
|
132
|
-
});
|
133
|
-
|
134
|
-
it('converts time from ISOString to Date', () => {
|
135
|
-
expect(storage.getLastUpdate().time).to.be.a('Date');
|
136
|
-
});
|
137
|
-
|
138
|
-
it('converts updateStarted from ISOString to Date', () => {
|
139
|
-
expect(storage.getLastUpdate().updateStarted).to.be.a('Date');
|
140
|
-
});
|
141
|
-
|
142
|
-
it('gives the right time', () => {
|
143
|
-
expect(storage.getLastUpdate().time.getTime()).to.be.equal(now.getTime());
|
144
|
-
});
|
145
|
-
|
146
|
-
it('gives the right updateStarted', () => {
|
147
|
-
expect(storage.getLastUpdate().updateStarted.getTime()).to.be.equal(now.getTime());
|
148
|
-
});
|
149
|
-
});
|
150
|
-
|
151
|
-
describe('getLastUpdate when updateStarted is false', () => {
|
152
|
-
beforeEach(() => {
|
153
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo:1, time: now.toISOString(), updateStarted: false});
|
154
|
-
});
|
155
|
-
|
156
|
-
it('gives the right updateStarted', () => {
|
157
|
-
expect(storage.getLastUpdate().updateStarted).equal(false);
|
158
|
-
});
|
159
|
-
});
|
160
|
-
|
161
|
-
describe('updateLastUpdate', () => {
|
162
|
-
describe('when updating time', () =>{
|
163
|
-
const date = new Date(2018, 5, 1, 11, 30, 0);
|
164
|
-
beforeEach(() => {
|
165
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, time: now.toISOString()});
|
166
|
-
storage.updateLastUpdate({time: date});
|
167
|
-
});
|
168
|
-
|
169
|
-
it('converts time from Date to ISOString', () => {
|
170
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).time).to.equal(date.toISOString());
|
171
|
-
});
|
172
|
-
|
173
|
-
it('leaves other properties unchanged', () => {
|
174
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
|
175
|
-
});
|
176
|
-
});
|
177
|
-
describe('when updating updateStarted with a Date', () =>{
|
178
|
-
const date = new Date(2018, 5, 1, 11, 30, 0);
|
179
|
-
beforeEach(() => {
|
180
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, updateStarted: now.toISOString()});
|
181
|
-
storage.updateLastUpdate({updateStarted: date});
|
182
|
-
});
|
183
|
-
|
184
|
-
it('converts updateStarted from Date to ISOString', () => {
|
185
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).updateStarted).to.equal(date.toISOString());
|
186
|
-
});
|
187
|
-
|
188
|
-
it('leaves other properties unchanged', () => {
|
189
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
|
190
|
-
});
|
191
|
-
});
|
192
|
-
describe('when updating updateStarted with false', () =>{
|
193
|
-
beforeEach(() => {
|
194
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, updateStarted: now.toISOString()});
|
195
|
-
storage.updateLastUpdate({updateStarted: false});
|
196
|
-
});
|
197
|
-
|
198
|
-
it('sets updateStarted to false', () => {
|
199
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).updateStarted).to.equal(false);
|
200
|
-
});
|
201
|
-
});
|
202
|
-
describe('when updating other properties', () =>{
|
203
|
-
beforeEach(() => {
|
204
|
-
mockStorage.myFTIndicatorUpdate = JSON.stringify({foo: 3, bar:4, baz:5});
|
205
|
-
storage.updateLastUpdate({bar: 5, baz:6});
|
206
|
-
});
|
207
|
-
|
208
|
-
it('updates existing properties', () => {
|
209
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).bar).to.equal(5);
|
210
|
-
});
|
211
|
-
|
212
|
-
it('add new properties', () => {
|
213
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).baz).to.equal(6);
|
214
|
-
});
|
215
|
-
|
216
|
-
it('leaves other properties unchanged', () => {
|
217
|
-
expect(JSON.parse(mockStorage.myFTIndicatorUpdate).foo).to.equal(3);
|
218
|
-
});
|
219
|
-
});
|
220
|
-
});
|
221
|
-
|
222
129
|
describe('isAvailable', () => {
|
223
130
|
it('should return true if it is available', () => {
|
224
131
|
sinon.stub(window.Storage.prototype, 'removeItem').callsFake((key) => delete mockStorage[key]);
|
@@ -1,79 +0,0 @@
|
|
1
|
-
{{#if @root.flags.myFtApiWrite}}
|
2
|
-
<form
|
3
|
-
class="n-myft-ui n-myft-ui--follow {{extraClasses}}"
|
4
|
-
method="GET"
|
5
|
-
data-myft-ui="follow"
|
6
|
-
data-concept-id="{{conceptId}}"
|
7
|
-
{{#if collectionName}}data-myft-tracking="collectionName={{collectionName}}"{{/if}}
|
8
|
-
{{#if followPlusDigestEmail}}
|
9
|
-
action="/__myft/api/core/follow-plus-digest-email/{{conceptId}}?method=put"
|
10
|
-
data-myft-ui-variant="followPlusDigestEmail"
|
11
|
-
{{else}}
|
12
|
-
{{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
|
13
|
-
action="/myft/remove/{{conceptId}}"
|
14
|
-
data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=delete"
|
15
|
-
{{else}}
|
16
|
-
action="/myft/add/{{conceptId}}"
|
17
|
-
data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put"
|
18
|
-
{{/ifAll}}
|
19
|
-
{{/if}}>
|
20
|
-
{{> n-myft-ui/components/csrf-token/input}}
|
21
|
-
<div
|
22
|
-
class="n-myft-ui__announcement o-normalise-visually-hidden"
|
23
|
-
aria-live="assertive"
|
24
|
-
data-pressed-text="Now following {{name}}."
|
25
|
-
data-unpressed-text="No longer following {{name}}."
|
26
|
-
></div>
|
27
|
-
<button
|
28
|
-
{{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
|
29
|
-
aria-label="Remove {{name}} from myFT"
|
30
|
-
title="Remove {{name}} from myFT"
|
31
|
-
data-alternate-label="Add {{name}} to myFT"
|
32
|
-
aria-pressed="true"
|
33
|
-
{{#if alternateText}}
|
34
|
-
data-alternate-text="{{alternateText}}"
|
35
|
-
{{else}}
|
36
|
-
{{#if buttonText}}
|
37
|
-
data-alternate-text="{{buttonText}}"
|
38
|
-
{{else}}
|
39
|
-
data-alternate-text="Add to myFT"
|
40
|
-
{{/if}}
|
41
|
-
{{/if}}
|
42
|
-
{{else}}
|
43
|
-
aria-pressed="false"
|
44
|
-
aria-label="Add {{name}} to myFT"
|
45
|
-
title="Add {{name}} to myFT"
|
46
|
-
data-alternate-label="Remove {{name}} from myFT"
|
47
|
-
{{#if alternateText}}
|
48
|
-
data-alternate-text="{{alternateText}}"
|
49
|
-
{{else}}
|
50
|
-
{{#if buttonText}}
|
51
|
-
data-alternate-text="{{buttonText}}"
|
52
|
-
{{else}}
|
53
|
-
data-alternate-text="Added"
|
54
|
-
{{/if}}
|
55
|
-
{{/if}}
|
56
|
-
{{/ifAll}}
|
57
|
-
class="{{extraButtonClasses}}
|
58
|
-
n-myft-follow-button
|
59
|
-
{{~#variant}} n-myft-follow-button--{{this}}{{/variant~}}"
|
60
|
-
data-concept-id="{{conceptId}}" {{! duplicated here for tracking}}
|
61
|
-
{{#if followPlusDigestEmail}}
|
62
|
-
data-trackable-context-messaging="add-to-myft-plus-digest-button"
|
63
|
-
{{/if}}
|
64
|
-
data-trackable="follow"
|
65
|
-
type="submit">
|
66
|
-
{{~#if buttonText~}}
|
67
|
-
{{buttonText}}
|
68
|
-
{{~else~}}
|
69
|
-
{{~#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl~}}
|
70
|
-
Added
|
71
|
-
{{~else~}}
|
72
|
-
Add to myFT
|
73
|
-
{{~/ifAll~}}
|
74
|
-
{{~/if~}}
|
75
|
-
</button>
|
76
|
-
</form>
|
77
|
-
{{else}}
|
78
|
-
<!-- Add to myFT button hidden due to myFtApiWrite being off -->
|
79
|
-
{{/if}}
|
@@ -1,37 +0,0 @@
|
|
1
|
-
import Tooltip from 'o-tooltip';
|
2
|
-
import myftClient from 'next-myft-client';
|
3
|
-
|
4
|
-
const isMyftPage = window.location.pathname.startsWith('/myft');
|
5
|
-
const externalReferrer = !document.referrer || !(new URL(document.referrer).hostname.endsWith('ft.com'));
|
6
|
-
const myftHeaderLogo = document.querySelector('.o-header__top-link--myft');
|
7
|
-
const flagIsEnabled = window.FT && window.FT.flags && window.FT.flags.get('myFT_HeaderTooltip');
|
8
|
-
|
9
|
-
export const init = () => {
|
10
|
-
if (!externalReferrer || isMyftPage || !myftHeaderLogo || !flagIsEnabled) {
|
11
|
-
return;
|
12
|
-
}
|
13
|
-
|
14
|
-
return myftClient.getAll('followed', 'concept')
|
15
|
-
.then(followedConcepts => {
|
16
|
-
if (followedConcepts.length) {
|
17
|
-
const concepts = followedConcepts
|
18
|
-
.sort((a, b) => b.lastPublished - a.lastPublished)
|
19
|
-
.map(({name}) => `<span style="no-wrap">${name}</span>`).slice(0, 3);
|
20
|
-
let content = 'Read the latest on ';
|
21
|
-
|
22
|
-
if (concepts.length === 3) {
|
23
|
-
content += `${concepts.shift()}, `;
|
24
|
-
}
|
25
|
-
|
26
|
-
content += concepts.join(' and ');
|
27
|
-
content += ' stories.';
|
28
|
-
|
29
|
-
new Tooltip(myftHeaderLogo, {
|
30
|
-
target: 'myft-header-tooltip',
|
31
|
-
content: content,
|
32
|
-
showOnConstruction: true,
|
33
|
-
position: 'below'
|
34
|
-
});
|
35
|
-
}
|
36
|
-
});
|
37
|
-
};
|
@@ -1,26 +0,0 @@
|
|
1
|
-
import {json as fetchJson} from 'fetchres';
|
2
|
-
import {isAfter, parseISO} from './date-fns';
|
3
|
-
|
4
|
-
export default async (userId, startTime) => {
|
5
|
-
const articles = await fetchContentFromPersonalisedFeed(userId);
|
6
|
-
const unreadArticlesSinceLastVisit = articles
|
7
|
-
.filter(({userCompletion = -1}) => userCompletion < 1) // only include unread articles
|
8
|
-
.filter(({contentTimeStamp}) => isAfter(parseISO(contentTimeStamp), startTime)); // only include articles published since last visit
|
9
|
-
|
10
|
-
return unreadArticlesSinceLastVisit.length;
|
11
|
-
};
|
12
|
-
|
13
|
-
async function fetchContentFromPersonalisedFeed (userId) {
|
14
|
-
|
15
|
-
const searchParams = new URLSearchParams({
|
16
|
-
originatingSignals: 'followed',
|
17
|
-
from: '-1d',
|
18
|
-
source: 'myft-ui'
|
19
|
-
});
|
20
|
-
|
21
|
-
const res = await fetch(`/__myft/api/onsite/feed/${userId}?${searchParams.toString()}`, {
|
22
|
-
credentials: 'include'
|
23
|
-
});
|
24
|
-
const {results = []} = await fetchJson(res);
|
25
|
-
return results;
|
26
|
-
}
|