Added Secondary Navigation (#1410)

refs: https://github.com/TryGhost/Ghost/pull/11409

- Added a new UI for a second set of navigation links
- This should support most concepts of nav, e.g. left and right, or header and footer
- This PR mostly updates the design and nav components to cope with a second set of nav
This commit is contained in:
Hannah Wolfe 2019-12-04 04:14:45 +00:00 committed by Naz Gargol
parent a50cc62617
commit 00ce91ce3a
10 changed files with 174 additions and 44 deletions

View File

@ -23,8 +23,8 @@ export default Component.extend(ValidationState, {
}),
actions: {
addItem() {
this.addItem();
addItem(item) {
this.addItem(item);
},
deleteItem(item) {
@ -53,7 +53,7 @@ export default Component.extend(ValidationState, {
if (event.keyCode === 13 && this.get('navItem.isNew')) {
event.preventDefault();
run.scheduleOnce('actions', this, function () {
this.send('addItem');
this.send('addItem', this.get('navItem'));
});
}
}

View File

@ -19,12 +19,14 @@ export default Controller.extend({
dirtyAttributes: false,
newNavItem: null,
newSecondaryNavItem: null,
themes: null,
themeToDelete: null,
init() {
this._super(...arguments);
this.set('newNavItem', NavigationItem.create({isNew: true}));
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
},
showDeleteThemeModal: notEmpty('themeToDelete'),
@ -40,16 +42,14 @@ export default Controller.extend({
this.save.perform();
},
addNavItem() {
let newNavItem = this.newNavItem;
addNavItem(item) {
// If the url sent through is blank (user never edited the url)
if (newNavItem.get('url') === '') {
newNavItem.set('url', '/');
if (item.get('url') === '') {
item.set('url', '/');
}
return newNavItem.validate().then(() => {
this.addNewNavItem();
return item.validate().then(() => {
this.addNewNavItem(item);
});
},
@ -58,7 +58,7 @@ export default Controller.extend({
return;
}
let navItems = this.get('settings.navigation');
let navItems = item.isSecondary ? this.get('settings.secondaryNavigation') : this.get('settings.navigation');
navItems.removeObject(item);
this.set('dirtyAttributes', true);
@ -196,23 +196,33 @@ export default Controller.extend({
reset() {
this.set('newNavItem', NavigationItem.create({isNew: true}));
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
}
},
save: task(function* () {
let navItems = this.get('settings.navigation');
let newNavItem = this.newNavItem;
let secondaryNavItems = this.get('settings.secondaryNavigation');
let notifications = this.notifications;
let validationPromises = [];
if (!newNavItem.get('isBlank')) {
validationPromises.pushObject(this.send('addNavItem'));
if (!this.newNavItem.get('isBlank')) {
validationPromises.pushObject(this.send('addNavItem', this.newNavItem));
}
if (!this.newSecondaryNavItem.get('isBlank')) {
validationPromises.pushObject(this.send('addNavItem', this.newSecondaryNavItem));
}
navItems.map((item) => {
validationPromises.pushObject(item.validate());
});
secondaryNavItems.map((item) => {
validationPromises.pushObject(item.validate());
});
try {
yield RSVP.all(validationPromises);
this.set('dirtyAttributes', false);
@ -225,15 +235,20 @@ export default Controller.extend({
}
}),
addNewNavItem() {
let navItems = this.get('settings.navigation');
let newNavItem = this.newNavItem;
addNewNavItem(item) {
let navItems = item.isSecondary ? this.get('settings.secondaryNavigation') : this.get('settings.navigation');
newNavItem.set('isNew', false);
navItems.pushObject(newNavItem);
item.set('isNew', false);
navItems.pushObject(item);
this.set('dirtyAttributes', true);
this.set('newNavItem', NavigationItem.create({isNew: true}));
$('.gh-blognav-line:last input:first').focus();
if (item.isSecondary) {
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
$('.gh-blognav-container:last .gh-blognav-line:last input:first').focus();
} else {
this.set('newNavItem', NavigationItem.create({isNew: true}));
$('.gh-blognav-container:first .gh-blognav-line:last input:first').focus();
}
},
_deleteTheme() {

View File

@ -7,6 +7,7 @@ export default EmberObject.extend(ValidationEngine, {
label: '',
url: '',
isNew: false,
isSecondary: false,
validationType: 'navItem',

View File

@ -19,6 +19,7 @@ export default Model.extend(ValidationEngine, {
twitter: attr('twitter-url-user'),
labs: attr('string'),
navigation: attr('navigation-settings'),
secondaryNavigation: attr('navigation-settings', {isSecondary: true}),
isPrivate: attr('boolean'),
publicHash: attr('string'),
password: attr('string'),

View File

@ -32,7 +32,7 @@
</div>
{{#if navItem.isNew}}
<button type="button" class="gh-blognav-add" {{action "addItem"}}>
<button type="button" class="gh-blognav-add" {{action "addItem" navItem}}>
{{svg-jar "add"}}<span class="sr-only">Add</span>
</button>
{{else}}

View File

@ -16,7 +16,6 @@
{{/if}}
<section class="view-container">
<div class="gh-setting-header gh-first-header">Navigation</div>
<div class="gh-blognav-container pa5 pt6 bg-grouped-table shadow-1 br3">
<form id="settings-navigation" class="gh-blognav" novalidate="novalidate">
@ -43,6 +42,32 @@
</form>
</div>
<div class="gh-setting-header">Secondary Navigation</div>
<div class="gh-blognav-container pa5 pt6 bg-grouped-table shadow-1 br3">
<form id="secondary-navigation" class="gh-blognav" novalidate="novalidate">
{{#sortable-objects sortableObjectList=settings.secondaryNavigation useSwap=false}}
{{#each settings.secondaryNavigation as |navItem index|}}
{{#draggable-object content=navItem dragHandle=".gh-blognav-grab" isSortable=true}}
{{gh-navitem
navItem=navItem
baseUrl=blogUrl
addItem=(action "addNavItem")
deleteItem=(action "deleteNavItem")
updateUrl=(action "updateUrl")
updateLabel=(action "updateLabel")
data-test-navitem=index}}
{{/draggable-object}}
{{/each}}
{{/sortable-objects}}
{{gh-navitem
navItem=newSecondaryNavItem
baseUrl=blogUrl
addItem=(action "addNavItem")
updateUrl=(action "updateUrl")
data-test-navitem="new"}}
</form>
</div>
<div class="gh-setting-header">Theme Directory</div>
<div class="gh-theme-directory-container">
<div class="theme-directory">

View File

@ -3,7 +3,7 @@ import Transform from 'ember-data/transform';
import {A as emberA, isArray as isEmberArray} from '@ember/array';
export default Transform.extend({
deserialize(serialized) {
deserialize(serialized, options) {
let navItems, settingsArray;
try {
@ -12,7 +12,10 @@ export default Transform.extend({
settingsArray = [];
}
navItems = settingsArray.map(itemDetails => NavigationItem.create(itemDetails));
navItems = settingsArray.map((itemDetails) => {
itemDetails.isSecondary = options && options.isSecondary || false;
return NavigationItem.create(itemDetails);
});
return emberA(navItems);
},

View File

@ -192,5 +192,15 @@ export default [
created_by: 1,
updated_at: '2019-10-09T09:49:00.000Z',
updated_by: 1
},
{
id: 25,
key: 'secondary_navigation',
type: 'blog',
created_at: '2019-11-20T09:44:30.810Z',
created_by: 1,
updated_at: '2019-11-20T13:32:49.868Z',
updated_by: 1,
value: JSON.stringify([])
}
];

View File

@ -60,18 +60,18 @@ describe('Acceptance: Settings - Design', function () {
expect(currentRouteName()).to.equal('settings.design.index');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save');
// fixtures contain two nav items, check for three rows as we
// should have one extra that's blank
// fixtures contain two nav items, check for four rows as we
// should have one extra that's blank for each navigation section
expect(
findAll('[data-test-navitem]').length,
'navigation items count'
).to.equal(3);
).to.equal(4);
});
it('saves navigation settings', async function () {
await visit('/settings/design');
await fillIn('[data-test-navitem="0"] [data-test-input="label"]', 'Test');
await typeIn('[data-test-navitem="0"] [data-test-input="url"]', '/test');
await fillIn('#settings-navigation [data-test-navitem="0"] [data-test-input="label"]', 'Test');
await typeIn('#settings-navigation [data-test-navitem="0"] [data-test-input="url"]', '/test');
await click('[data-test-save-button]');
let [navSetting] = this.server.db.settings.where({key: 'navigation'});
@ -91,23 +91,23 @@ describe('Acceptance: Settings - Design', function () {
await click('[data-test-save-button]');
expect(
findAll('[data-test-navitem]').length,
findAll('#settings-navigation [data-test-navitem]').length,
'number of nav items after saving with blank new item'
).to.equal(3);
await fillIn('[data-test-navitem="new"] [data-test-input="label"]', 'Test');
await fillIn('[data-test-navitem="new"] [data-test-input="url"]', '');
await typeIn('[data-test-navitem="new"] [data-test-input="url"]', 'http://invalid domain/');
await fillIn('#settings-navigation [data-test-navitem="new"] [data-test-input="label"]', 'Test');
await fillIn('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]', '');
await typeIn('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]', 'http://invalid domain/');
await click('[data-test-save-button]');
expect(
findAll('[data-test-navitem]').length,
findAll('#settings-navigation [data-test-navitem]').length,
'number of nav items after saving with invalid new item'
).to.equal(3);
expect(
withText(findAll('[data-test-navitem="new"] [data-test-error]')).length,
withText(findAll('#settings-navigation [data-test-navitem="new"] [data-test-error]')).length,
'number of invalid fields in new item'
).to.equal(1);
});
@ -135,7 +135,7 @@ describe('Acceptance: Settings - Design', function () {
it('can add and remove items', async function () {
await visit('/settings/design');
await click('.gh-blognav-add');
await click('#settings-navigation .gh-blognav-add');
expect(
find('[data-test-navitem="new"] [data-test-error="label"]').textContent.trim(),
@ -166,17 +166,17 @@ describe('Acceptance: Settings - Design', function () {
await click('.gh-blognav-add');
expect(
findAll('[data-test-navitem]').length,
findAll('#settings-navigation [data-test-navitem]').length,
'number of nav items after successful add'
).to.equal(4);
expect(
find('[data-test-navitem="new"] [data-test-input="label"]').value,
find('#settings-navigation [data-test-navitem="new"] [data-test-input="label"]').value,
'new item label value after successful add'
).to.be.empty;
expect(
find('[data-test-navitem="new"] [data-test-input="url"]').value,
find('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]').value,
'new item url value after successful add'
).to.equal(`${window.location.origin}/`);
@ -185,10 +185,10 @@ describe('Acceptance: Settings - Design', function () {
'number or validation errors shown after successful add'
).to.equal(0);
await click('[data-test-navitem="0"] .gh-blognav-delete');
await click('#settings-navigation [data-test-navitem="0"] .gh-blognav-delete');
expect(
findAll('[data-test-navitem]').length,
findAll('#settings-navigation [data-test-navitem]').length,
'number of nav items after successful remove'
).to.equal(3);
@ -204,6 +204,81 @@ describe('Acceptance: Settings - Design', function () {
expect(navSetting.value).to.equal('[{"label":"About","url":"/about"},{"label":"New","url":"/new/"}]');
});
it('can also add and remove items from seconday nav', async function () {
await visit('/settings/design');
await click('#secondary-navigation .gh-blognav-add');
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-error="label"]').textContent.trim(),
'blank label has validation error'
).to.not.be.empty;
await fillIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]', '');
await typeIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]', 'Foo');
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-error="label"]').textContent.trim(),
'label validation is visible after typing'
).to.be.empty;
await fillIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]', '');
await typeIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]', '/bar');
await blur('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]');
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-error="url"]').textContent.trim(),
'url validation is visible after typing'
).to.be.empty;
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]').value
).to.equal(`${window.location.origin}/bar/`);
await click('[data-test-save-button]');
expect(
findAll('#secondary-navigation [data-test-navitem]').length,
'number of nav items after successful add'
).to.equal(2);
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]').value,
'new item label value after successful add'
).to.be.empty;
expect(
find('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]').value,
'new item url value after successful add'
).to.equal(`${window.location.origin}/`);
expect(
withText(findAll('#secondary-navigation [data-test-navitem] [data-test-error]')).length,
'number or validation errors shown after successful add'
).to.equal(0);
let [navSetting] = this.server.db.settings.where({key: 'secondary_navigation'});
expect(navSetting.value).to.equal('[{"label":"Foo","url":"/bar/"}]');
await click('#secondary-navigation [data-test-navitem="0"] .gh-blognav-delete');
expect(
findAll('#secondary-navigation [data-test-navitem]').length,
'number of nav items after successful remove'
).to.equal(1);
// CMD-S shortcut works
await triggerEvent('.gh-app', 'keydown', {
keyCode: 83, // s
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
});
[navSetting] = this.server.db.settings.where({key: 'secondary_navigation'});
expect(navSetting.value).to.equal('[]');
});
it('allows management of themes', async function () {
// lists available themes + active theme is highlighted

View File

@ -102,7 +102,7 @@ describe('Unit: Controller: settings/design', function () {
ctrl.set('newNavItem.url', '/new');
run(() => {
ctrl.send('addNavItem');
ctrl.send('addNavItem', ctrl.get('newNavItem'));
});
expect(ctrl.get('settings.navigation.length')).to.equal(2);
@ -122,7 +122,7 @@ describe('Unit: Controller: settings/design', function () {
NavItem.create({label: '', url: '', last: true})
]}));
expect(ctrl.get('settings.navigation.length')).to.equal(1);
ctrl.send('addNavItem');
ctrl.send('addNavItem', ctrl.get('settings.navigation.lastObject'));
expect(ctrl.get('settings.navigation.length')).to.equal(1);
});
});