@financial-times/n-myft-ui 23.0.1 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
}
|