@financial-times/dotcom-server-navigation 7.3.1 → 7.3.3
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/package.json +5 -4
- package/src/__fixtures__/menus.ts +62 -0
- package/src/__test__/decorateMenuData.spec.ts +83 -0
- package/src/__test__/editions.spec.ts +25 -0
- package/src/__test__/navigation.spec.ts +134 -0
- package/src/__test__/selectMenuDataForEdition.spec.ts +38 -0
- package/src/actions.ts +9 -0
- package/src/decorateMenuData.ts +53 -0
- package/src/editions.ts +27 -0
- package/src/index.ts +3 -0
- package/src/navigation.ts +104 -0
- package/src/selectMenuDataForEdition.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/dotcom-server-navigation",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/node/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"author": "",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@financial-times/dotcom-types-navigation": "
|
|
21
|
+
"@financial-times/dotcom-types-navigation": "^7.3.3",
|
|
22
22
|
"@types/deep-freeze": "^0.1.1",
|
|
23
23
|
"deep-freeze": "0.0.1",
|
|
24
24
|
"ft-poller": "^7.2.1",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"npm": "7.x || 8.x"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
|
-
"dist/"
|
|
38
|
+
"dist/",
|
|
39
|
+
"src/"
|
|
39
40
|
],
|
|
40
41
|
"repository": {
|
|
41
42
|
"type": "git",
|
|
@@ -46,4 +47,4 @@
|
|
|
46
47
|
"volta": {
|
|
47
48
|
"extends": "../../package.json"
|
|
48
49
|
}
|
|
49
|
-
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TNavMenus, TNavMeganav, TNavMenu } from '@financial-times/dotcom-types-navigation'
|
|
2
|
+
|
|
3
|
+
const meganav: TNavMeganav[] = [
|
|
4
|
+
{
|
|
5
|
+
component: 'sectionlist',
|
|
6
|
+
dataset: 'subsections',
|
|
7
|
+
title: 'Sections',
|
|
8
|
+
data: [[{ label: 'Mega Foo', url: '/world' }], [{ label: 'Mega Bar', url: '/world/uk', submenu: null }]]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
component: 'articlelist',
|
|
12
|
+
dataset: 'popular',
|
|
13
|
+
title: 'Most Read',
|
|
14
|
+
data: [
|
|
15
|
+
{ label: 'Mega Baz', url: '/content/baz' },
|
|
16
|
+
{ label: 'Mega Qux', url: '/content/qux?location=${currentPath}' }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const submenu: TNavMenu = {
|
|
22
|
+
label: 'submenu',
|
|
23
|
+
items: [
|
|
24
|
+
{ label: 'Baz', url: '/world/uk', submenu: null },
|
|
25
|
+
{ label: 'Qux', url: '/fake-item-nested?location=${currentPath}', submenu: null }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const menus: Partial<TNavMenus> = {
|
|
30
|
+
'navbar-uk': {
|
|
31
|
+
label: 'Navigation',
|
|
32
|
+
items: [
|
|
33
|
+
{ label: 'Foo', url: '/world/uk', submenu: null },
|
|
34
|
+
{ label: 'Bar', url: '/fake-item?location=${currentPath}', submenu, meganav }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
footer: {
|
|
39
|
+
label: 'Footer',
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
label: 'Tools',
|
|
43
|
+
url: null,
|
|
44
|
+
submenu: {
|
|
45
|
+
label: null,
|
|
46
|
+
items: [
|
|
47
|
+
[
|
|
48
|
+
{ label: 'Alerts Hub', url: 'http://markets.ft.com/data/alerts/', submenu: null },
|
|
49
|
+
{ label: 'Lexicon', url: 'http://lexicon.ft.com/', submenu: null }
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
{ label: 'News feed', url: '/news-feed', submenu: null },
|
|
53
|
+
{ label: 'Newsletters', url: '/newsletters', submenu: null }
|
|
54
|
+
]
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { menus }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { decorateMenuData as subject } from '../decorateMenuData'
|
|
2
|
+
import { menus } from '../__fixtures__/menus'
|
|
3
|
+
import dlv from 'dlv'
|
|
4
|
+
|
|
5
|
+
describe('dotcom-server-navigation/src/decorateMenuData', () => {
|
|
6
|
+
describe('.decorateMenu()', () => {
|
|
7
|
+
it('returns a new deeply cloned object rather than mutating in place', () => {
|
|
8
|
+
const decorated = subject(menus['navbar-uk'], '/world/uk')
|
|
9
|
+
|
|
10
|
+
expect(decorated).not.toBe(menus['navbar-uk'])
|
|
11
|
+
expect(decorated.items).not.toBe(menus['navbar-uk'].items)
|
|
12
|
+
expect(decorated.items[0]).not.toBe(menus['navbar-uk'].items[0])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('marks menu items whose `url` property matches `currentPath` as `selected`', () => {
|
|
16
|
+
const decorated = subject(menus['navbar-uk'], '/world/uk')
|
|
17
|
+
|
|
18
|
+
const a = dlv(decorated, ['items', 0])
|
|
19
|
+
const b = dlv(decorated, ['items', 1, 'submenu', 'items', 0])
|
|
20
|
+
const c = dlv(decorated, ['items', 1, 'meganav', 0, 'data', 1, 0])
|
|
21
|
+
const d = dlv(decorated, ['items', 1])
|
|
22
|
+
|
|
23
|
+
expect(a.selected).toBe(true)
|
|
24
|
+
expect(b.selected).toBe(true)
|
|
25
|
+
expect(c.selected).toBe(true)
|
|
26
|
+
expect(d.selected).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('replaces the ${currentPath} placeholder with the value of `currentPath`', () => {
|
|
30
|
+
const decorated = subject(menus['navbar-uk'], '/world/us/politics')
|
|
31
|
+
|
|
32
|
+
const a = dlv(decorated, ['items', 1])
|
|
33
|
+
const b = dlv(decorated, ['items', 1, 'submenu', 'items', 1])
|
|
34
|
+
const c = dlv(decorated, ['items', 1, 'meganav', 1, 'data', 1])
|
|
35
|
+
|
|
36
|
+
expect(a.url).toBe('/fake-item?location=/world/us/politics')
|
|
37
|
+
expect(b.url).toBe('/fake-item-nested?location=/world/us/politics')
|
|
38
|
+
expect(c.url).toBe('/content/qux?location=/world/us/politics')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('replaces the ${currentPath} placeholder with %2F in URLs which contain keywords', () => {
|
|
42
|
+
const testKeyword = (itemUrl: string) => {
|
|
43
|
+
const decorated = subject(menus['navbar-uk'], itemUrl)
|
|
44
|
+
|
|
45
|
+
const a = dlv(decorated, ['items', 1, 'url'])
|
|
46
|
+
const b = dlv(decorated, ['items', 1, 'submenu', 'items', 1, 'url'])
|
|
47
|
+
|
|
48
|
+
expect(a).toBe('/fake-item?location=%2F')
|
|
49
|
+
expect(b).toBe('/fake-item-nested?location=%2F')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
testKeyword('/uk/products/bar')
|
|
53
|
+
testKeyword('/world/barriers')
|
|
54
|
+
testKeyword('/world/us/errors')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('can clone submenu properties', () => {
|
|
58
|
+
const decorated = subject(menus.footer, '/world/uk')
|
|
59
|
+
const submenu = dlv(decorated, ['items', 0, 'submenu', 'items'])
|
|
60
|
+
|
|
61
|
+
submenu.forEach((column) => {
|
|
62
|
+
column.forEach((menuItem) => {
|
|
63
|
+
expect(menuItem).toHaveProperty('label')
|
|
64
|
+
expect(menuItem).toHaveProperty('url')
|
|
65
|
+
expect(menuItem).toHaveProperty('selected')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('can clone meganav properties', () => {
|
|
71
|
+
const decorated = subject(menus['navbar-uk'], '/world/uk')
|
|
72
|
+
const meganav = dlv(decorated, ['items', 1, 'meganav'])
|
|
73
|
+
|
|
74
|
+
expect(meganav[0]).toHaveProperty('component', 'sectionlist')
|
|
75
|
+
expect(meganav[0]).toHaveProperty('title', 'Sections')
|
|
76
|
+
expect(meganav[0]).toHaveProperty('data')
|
|
77
|
+
|
|
78
|
+
expect(meganav[1]).toHaveProperty('component', 'articlelist')
|
|
79
|
+
expect(meganav[1]).toHaveProperty('title', 'Most Read')
|
|
80
|
+
expect(meganav[1]).toHaveProperty('data')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as subject from '../editions'
|
|
2
|
+
|
|
3
|
+
describe('dotcom-server-navigation/src/editions', () => {
|
|
4
|
+
describe('.isEdition()', () => {
|
|
5
|
+
it('returns true for editions which exist', () => {
|
|
6
|
+
expect(subject.isEdition('uk')).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns false for editions which do not exist', () => {
|
|
10
|
+
expect(subject.isEdition('london')).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('.getEditions()', () => {
|
|
15
|
+
it('returns the current selected edition', () => {
|
|
16
|
+
const result = subject.getEditions('uk')
|
|
17
|
+
expect(result.current).toEqual(expect.objectContaining({ id: 'uk' }))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns the all other editions', () => {
|
|
21
|
+
const result = subject.getEditions('uk')
|
|
22
|
+
expect(result.others).toEqual([expect.objectContaining({ id: 'international' })])
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import nock from 'nock'
|
|
2
|
+
import { Navigation } from '..'
|
|
3
|
+
import menusData from '../../../../__fixtures__/menus.json'
|
|
4
|
+
|
|
5
|
+
const subNavigationData = {
|
|
6
|
+
ancestors: [{ label: 'some-ancestors' }],
|
|
7
|
+
children: [{ label: 'some-children' }],
|
|
8
|
+
item: { label: 'current-page' }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FakePoller = {
|
|
12
|
+
start: jest.fn(),
|
|
13
|
+
getData: jest.fn()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
jest.mock('ft-poller', () => {
|
|
17
|
+
return jest.fn().mockImplementation(() => FakePoller)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const clone = (obj) => JSON.parse(JSON.stringify(obj))
|
|
21
|
+
|
|
22
|
+
describe('dotcom-server-navigation', () => {
|
|
23
|
+
let navigationInstance
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
FakePoller.start.mockResolvedValue(null)
|
|
27
|
+
FakePoller.getData.mockResolvedValue(clone(menusData))
|
|
28
|
+
|
|
29
|
+
navigationInstance = new Navigation()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
nock.cleanAll()
|
|
34
|
+
jest.clearAllMocks()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('constructor', () => {
|
|
38
|
+
it('initialises the poller', () => {
|
|
39
|
+
expect(FakePoller.start).toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('.getMenusData()', () => {
|
|
44
|
+
let result
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
result = await navigationInstance.getMenusData()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns the raw menus data', () => {
|
|
51
|
+
expect(result).toEqual(menusData)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('.getMenusFor()', () => {
|
|
56
|
+
let result
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
result = await navigationInstance.getMenusFor('/', 'uk')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns the shared menu data', () => {
|
|
63
|
+
expect(result).toHaveProperty('account.label', 'Account')
|
|
64
|
+
expect(result).toHaveProperty('footer.label', 'Footer')
|
|
65
|
+
expect(result).toHaveProperty('user.label', 'User')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('returns the edition specific menu data', () => {
|
|
69
|
+
expect(result).toHaveProperty('drawer.label', 'Drawer')
|
|
70
|
+
expect(result).toHaveProperty('navbar.label', 'Navigation')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns decorated menu data', () => {
|
|
74
|
+
expect(result).toHaveProperty('navbar.items.0.selected', true)
|
|
75
|
+
expect(result).toHaveProperty('drawer.items.0.submenu.items.0.selected', true)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// nock used here because SubNavigation fetches its data directly rather than pulling from Poller
|
|
80
|
+
describe('.getSubNavigationFor()', () => {
|
|
81
|
+
let result
|
|
82
|
+
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
nock('http://next-navigation.ft.com')
|
|
85
|
+
.get('/v2/hierarchy/streamPage')
|
|
86
|
+
.reply(200, clone(subNavigationData))
|
|
87
|
+
|
|
88
|
+
result = await navigationInstance.getSubNavigationFor('/streamPage')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('fetches the sub-navigation data', () => {
|
|
92
|
+
expect(result).toHaveProperty('breadcrumb')
|
|
93
|
+
expect(result).toHaveProperty('subsections')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('appends the current page to the list of ancestors', () => {
|
|
97
|
+
expect(result).toHaveProperty('breadcrumb.1.label', 'current-page')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('appends a selected property to the current page', () => {
|
|
101
|
+
expect(result).toHaveProperty('breadcrumb.1.selected', true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('when things go wrong', () => {
|
|
105
|
+
it('throws an HTTP error when fetch fails', async () => {
|
|
106
|
+
nock('http://next-navigation.ft.com').get('/v2/hierarchy/streamPage').reply(500)
|
|
107
|
+
|
|
108
|
+
await expect(navigationInstance.getSubNavigationFor('streamPage')).rejects.toMatchObject({
|
|
109
|
+
message: 'Sub-navigation for streamPage could not be found.'
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('.getEditionsFor()', () => {
|
|
116
|
+
let result
|
|
117
|
+
|
|
118
|
+
beforeAll(async () => {
|
|
119
|
+
result = await navigationInstance.getEditionsFor('uk')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('returns the editions data', () => {
|
|
123
|
+
expect(result).toHaveProperty('current')
|
|
124
|
+
expect(result).toHaveProperty('others')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('with an invalid edition', () => {
|
|
128
|
+
it('throws an error', () => {
|
|
129
|
+
const test = () => navigationInstance.getEditionsFor('london')
|
|
130
|
+
expect(test).toThrow('The provided edition "london" is not a valid edition')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { selectMenuDataForEdition as subject } from '../selectMenuDataForEdition'
|
|
2
|
+
import { TNavMenus } from '@financial-times/dotcom-types-navigation'
|
|
3
|
+
|
|
4
|
+
const fixture = {
|
|
5
|
+
account: {},
|
|
6
|
+
anon: {},
|
|
7
|
+
footer: {},
|
|
8
|
+
'drawer-international': {},
|
|
9
|
+
'drawer-uk': {},
|
|
10
|
+
'navbar-simple': {},
|
|
11
|
+
'navbar-right': {},
|
|
12
|
+
'navbar-right-anon': {},
|
|
13
|
+
'navbar-international': {},
|
|
14
|
+
'navbar-uk': {},
|
|
15
|
+
user: {}
|
|
16
|
+
} as TNavMenus
|
|
17
|
+
|
|
18
|
+
describe('dotcom-server-navigation/src/selectMenuDataForEdition', () => {
|
|
19
|
+
it('returns a new object', () => {
|
|
20
|
+
const result = subject(fixture, 'uk')
|
|
21
|
+
expect(result).not.toBe(fixture)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('appends shared menus', () => {
|
|
25
|
+
const result = subject(fixture, 'uk')
|
|
26
|
+
|
|
27
|
+
expect(result.account).toBe(fixture.account)
|
|
28
|
+
expect(result.footer).toBe(fixture.footer)
|
|
29
|
+
expect(result.user).toBe(fixture.user)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('appends edition specific menus', () => {
|
|
33
|
+
const result = subject(fixture, 'uk')
|
|
34
|
+
|
|
35
|
+
expect(result.drawer).toBe(fixture['drawer-uk'])
|
|
36
|
+
expect(result.navbar).toBe(fixture['navbar-uk'])
|
|
37
|
+
})
|
|
38
|
+
})
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TNavAction } from '@financial-times/dotcom-types-navigation'
|
|
2
|
+
|
|
3
|
+
export function getSubscribeAction(): TNavAction {
|
|
4
|
+
return {
|
|
5
|
+
id: 'subscribe',
|
|
6
|
+
name: 'Subscribe for full access',
|
|
7
|
+
url: '/products?segmentId=4526c036-7527-ab37-9a29-0b0403fa0b5f'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TNavMenuItem } from '@financial-times/dotcom-types-navigation'
|
|
2
|
+
|
|
3
|
+
const isSelected = (url: string, currentPath: string): boolean => {
|
|
4
|
+
return url === currentPath
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const isMenuItem = (item: any): boolean => {
|
|
8
|
+
return item.hasOwnProperty('label') && item.hasOwnProperty('url')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const decorateURL = (url: string, currentPath: string): string => {
|
|
12
|
+
if (url && url.includes('${currentPath}')) {
|
|
13
|
+
// Don't replace the URL placeholder with a barrier or error URL so that
|
|
14
|
+
// a user logging in is not redirected to a barrier or error!
|
|
15
|
+
const shouldReplace = !/\/(products|barriers|errors)/.test(currentPath)
|
|
16
|
+
const redirectPath = shouldReplace ? currentPath : '%2F'
|
|
17
|
+
return url.replace('${currentPath}', redirectPath)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return url
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const decorateMenuItem = (item: TNavMenuItem, currentPath: string): void => {
|
|
24
|
+
item.url = decorateURL(item.url, currentPath)
|
|
25
|
+
item.selected = isSelected(item.url, currentPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// This is a recursive clone function that will decorate objects it recognises as links
|
|
29
|
+
function cloneMenu(value: any, currentPath: string): any {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return value.map((item) => cloneMenu(item, currentPath))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Object(value) === value) {
|
|
35
|
+
const cloned = {}
|
|
36
|
+
|
|
37
|
+
for (const key of Object.keys(value)) {
|
|
38
|
+
cloned[key] = cloneMenu(value[key], currentPath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isMenuItem(cloned)) {
|
|
42
|
+
decorateMenuItem(cloned as TNavMenuItem, currentPath)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return cloned
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function decorateMenuData<Type>(menuData: Type, currentUrl: string): Type {
|
|
52
|
+
return cloneMenu(menuData, currentUrl)
|
|
53
|
+
}
|
package/src/editions.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TNavEdition, TNavEditions } from '@financial-times/dotcom-types-navigation'
|
|
2
|
+
|
|
3
|
+
const availableEditions: TNavEdition[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'uk',
|
|
6
|
+
name: 'UK',
|
|
7
|
+
url: '/'
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: 'international',
|
|
11
|
+
name: 'International',
|
|
12
|
+
url: '/'
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const editionIDs = availableEditions.map((edition) => edition.id)
|
|
17
|
+
|
|
18
|
+
export function isEdition(editionID: string): boolean {
|
|
19
|
+
return editionIDs.includes(editionID)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getEditions(currentEdition: string): TNavEditions {
|
|
23
|
+
return {
|
|
24
|
+
current: availableEditions.find((edition) => edition.id === currentEdition),
|
|
25
|
+
others: availableEditions.filter((edition) => edition.id !== currentEdition)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import Poller from 'ft-poller'
|
|
2
|
+
import httpError from 'http-errors'
|
|
3
|
+
import deepFreeze from 'deep-freeze'
|
|
4
|
+
import fetch from 'node-fetch'
|
|
5
|
+
import {
|
|
6
|
+
TNavMenus,
|
|
7
|
+
TNavMenusForEdition,
|
|
8
|
+
TNavSubNavigation,
|
|
9
|
+
TNavEditions,
|
|
10
|
+
TNavAction
|
|
11
|
+
} from '@financial-times/dotcom-types-navigation'
|
|
12
|
+
import { selectMenuDataForEdition } from './selectMenuDataForEdition'
|
|
13
|
+
import { decorateMenuData } from './decorateMenuData'
|
|
14
|
+
import { getEditions, isEdition } from './editions'
|
|
15
|
+
import { getSubscribeAction } from './actions'
|
|
16
|
+
|
|
17
|
+
// Makes the navigation data completely immutable,
|
|
18
|
+
// To modify the data, clone the parts you need to change then modify in your app
|
|
19
|
+
const parseData = (data: any) => {
|
|
20
|
+
return deepFreeze(data)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const removeLeadingForwardSlash = (pagePath: string) => {
|
|
24
|
+
return pagePath.charAt(0) === '/' ? pagePath.substring(1) : pagePath
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type TNavOptions = {
|
|
28
|
+
menuUrl?: string
|
|
29
|
+
subNavigationUrl?: string
|
|
30
|
+
interval?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const defaults: TNavOptions = {
|
|
34
|
+
menuUrl: 'http://next-navigation.ft.com/v2/menus',
|
|
35
|
+
subNavigationUrl: 'http://next-navigation.ft.com/v2/hierarchy',
|
|
36
|
+
interval: 15 * 60 * 1000 // poll every 15 minutes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Navigation {
|
|
40
|
+
public options: TNavOptions
|
|
41
|
+
public poller: Poller
|
|
42
|
+
public initialPromise: Promise<void>
|
|
43
|
+
private menuData: TNavMenus
|
|
44
|
+
|
|
45
|
+
constructor(options: TNavOptions = {}) {
|
|
46
|
+
this.options = { ...defaults, ...options }
|
|
47
|
+
|
|
48
|
+
this.poller = new Poller({
|
|
49
|
+
url: this.options.menuUrl,
|
|
50
|
+
refreshInterval: this.options.interval,
|
|
51
|
+
parseData
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
this.initialPromise = this.poller.start({ initialRequest: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getMenusData(): Promise<TNavMenus> {
|
|
58
|
+
// initialPromise does not return data but must resolve before `getData` can be called
|
|
59
|
+
await this.initialPromise
|
|
60
|
+
|
|
61
|
+
this.menuData = this.poller.getData()
|
|
62
|
+
|
|
63
|
+
return this.menuData
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getMenusFor(currentPath: string, currentEdition: string = 'uk'): Promise<TNavMenusForEdition> {
|
|
68
|
+
const menusData = await this.getMenusData()
|
|
69
|
+
const menusForEdition = selectMenuDataForEdition(menusData, currentEdition)
|
|
70
|
+
|
|
71
|
+
return decorateMenuData(menusForEdition, currentPath)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getSubNavigationFor(path: string): Promise<TNavSubNavigation> {
|
|
75
|
+
const currentPage = removeLeadingForwardSlash(path)
|
|
76
|
+
const subNavigation = `${this.options.subNavigationUrl}/${currentPage}`
|
|
77
|
+
const response = await fetch(subNavigation)
|
|
78
|
+
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const data = await response.json()
|
|
81
|
+
|
|
82
|
+
const currentItem = { ...data.item, selected: true }
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
breadcrumb: data.ancestors.concat(currentItem),
|
|
86
|
+
subsections: data.children
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
throw httpError(response.status, `Sub-navigation for ${currentPage} could not be found.`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getEditionsFor(currentEdition: string = 'uk'): TNavEditions {
|
|
94
|
+
if (isEdition(currentEdition)) {
|
|
95
|
+
return getEditions(currentEdition)
|
|
96
|
+
} else {
|
|
97
|
+
throw Error(`The provided edition "${currentEdition}" is not a valid edition`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getSubscribeAction(): TNavAction {
|
|
102
|
+
return getSubscribeAction()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TNavMenuKeys, TNavMenus, TNavMenusForEdition } from '@financial-times/dotcom-types-navigation'
|
|
2
|
+
|
|
3
|
+
const sharedMenuKeys: TNavMenuKeys[] = [
|
|
4
|
+
'account',
|
|
5
|
+
'anon',
|
|
6
|
+
'footer',
|
|
7
|
+
'navbar-simple',
|
|
8
|
+
'navbar-right',
|
|
9
|
+
'navbar-right-anon',
|
|
10
|
+
'user'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export function selectMenuDataForEdition(menuData: TNavMenus, currentEdition: string): TNavMenusForEdition {
|
|
14
|
+
const output = {
|
|
15
|
+
navbar: menuData[`navbar-${currentEdition}`],
|
|
16
|
+
drawer: menuData[`drawer-${currentEdition}`]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const key of sharedMenuKeys) {
|
|
20
|
+
if (menuData[key]) {
|
|
21
|
+
output[key] = menuData[key]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return output as TNavMenusForEdition
|
|
26
|
+
}
|