Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
{{@publishOptions.post.displayName}}

{{#if this.willPublish}}
will be published on your site{{#if this.willEmail}}, and delivered to{{else}}.{{/if}}
will be published on your site{{#if this.willEmail}}, and delivered to{{else if @publishOptions.desiredNavigationPlacement}}
and listed in your <strong>{{@publishOptions.desiredNavigationPlacement}} navigation</strong>.
{{else}}.{{/if}}
{{/if}}

{{#if this.willEmail}}
Expand Down
21 changes: 21 additions & 0 deletions ghost/admin/app/components/editor/modals/publish-flow/options.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@
</div>
{{/if}}

{{#if @publishOptions.showNavigationOption}}
<div class="gh-publish-setting" data-test-setting="navigation">
<button type="button" class="gh-publish-setting-title" {{on "click" (fn this.toggleSection "navigation")}} data-test-setting-title>
{{svg-jar "navigation"}}
<div class="gh-publish-setting-trigger">
<span>{{@publishOptions.selectedNavigationOption.display}}</span>
</div>
<span class="{{if (eq this.openSection "navigation") "expanded"}}">
{{svg-jar "arrow-down" class="icon-expand"}}
</span>
</button>
{{#liquid-if (eq this.openSection "navigation")}}
<div class="gh-publish-setting-form">
<Editor::PublishOptions::Navigation
@publishOptions={{@publishOptions}}
/>
</div>
{{/liquid-if}}
</div>
{{/if}}

<div class="gh-publish-setting last" data-test-setting="publish-at">
<button type="button" class="gh-publish-setting-title {{if @publishOptions.publishDisabledError "disabled"}}" {{on "click" (if @publishOptions.publishDisabledError (optional) (fn this.toggleSection "publishAt"))}} data-test-setting-title>
{{svg-jar "clock"}}
Expand Down
10 changes: 10 additions & 0 deletions ghost/admin/app/components/editor/publish-management.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default class PublishManagement extends Component {

if (isValid && (!this.publishFlowModal || this.publishFlowModal?.isClosing)) {
this.publishOptions.resetPastScheduledAt();
this.publishOptions.resetNavigationPlacement();

this.publishFlowModal = this.modals.open(PublishFlowModal, {
publishOptions: this.publishOptions,
Expand Down Expand Up @@ -209,6 +210,15 @@ export default class PublishManagement extends Component {
// save with the required query params for emailing
const result = yield this.publishOptions[taskName].perform();

// the page published fine but its navigation placement couldn't be
// saved - surface that quietly rather than failing the publish
if (taskName === 'saveTask' && this.publishOptions.navigationSaveFailed) {
this.notifications.showNotification(
'Page published, but its navigation couldn\'t be updated. You can change it in Settings → Navigation.',
{type: 'error', delayed: true}
);
}

// perform any post-save cleanup for the editor
yield this.args.afterPublish(result);

Expand Down
17 changes: 17 additions & 0 deletions ghost/admin/app/components/editor/publish-options/navigation.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<fieldset class="gh-publish-types">
{{#each @publishOptions.navigationOptions as |option|}}
<span>
<input
type="radio"
name="navigation-placement"
id="navigation-placement-{{option.value}}"
class="gh-radio-button"
value={{option.value}}
checked={{eq option.value @publishOptions.selectedNavigationOption.value}}
{{on "change" this.onChange}}
data-test-navigation-placement={{option.value}}
>
<label for="navigation-placement-{{option.value}}">{{option.label}}</label>
</span>
{{/each}}
</fieldset>
10 changes: 10 additions & 0 deletions ghost/admin/app/components/editor/publish-options/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';

export default class NavigationOption extends Component {
@action
onChange(event) {
event.preventDefault();
this.args.publishOptions.setNavigationPlacement(event.target.value);
}
}
24 changes: 24 additions & 0 deletions ghost/admin/app/components/posts-list/context-menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@
</li>
{{/if}}

{{#if this.canManageNavigation}}
{{#if this.canAddToPrimaryNavigation}}
<li>
<button class="mr2" type="button" {{on "click" this.addToPrimaryNavigation}} data-test-button="add-to-primary-navigation">
<span>{{svg-jar "navigation"}}{{this.primaryNavigationActionLabel}}</span>
</button>
</li>
{{/if}}
{{#if this.canAddToSecondaryNavigation}}
<li>
<button class="mr2" type="button" {{on "click" this.addToSecondaryNavigation}} data-test-button="add-to-secondary-navigation">
<span>{{svg-jar "navigation"}}{{this.secondaryNavigationActionLabel}}</span>
</button>
</li>
{{/if}}
{{#if this.canRemoveFromNavigation}}
<li>
<button class="mr2" type="button" {{on "click" this.removeFromNavigation}} data-test-button="remove-from-navigation">
<span>{{svg-jar "close-stroke"}}Remove from navigation</span>
</button>
</li>
{{/if}}
{{/if}}

{{#if this.canCopySelection}}
<li>
<button class="mr2" type="button" {{on "click" this.copyPosts}} data-test-button="duplicate">
Expand Down
99 changes: 99 additions & 0 deletions ghost/admin/app/components/posts-list/context-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import nql from '@tryghost/nql';
import {action} from '@ember/object';
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
import {getPagePlacement, pagePathForSlug, setPagesNavigationPlacement} from 'ghost-admin/utils/site-navigation';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';

Expand Down Expand Up @@ -66,6 +68,9 @@ export default class PostsContextMenu extends Component {
@service store;
@service notifications;
@service membersUtils;
@service settings;

@inject config;

get menu() {
return this.args.menu;
Expand Down Expand Up @@ -494,4 +499,98 @@ export default class PostsContextMenu extends Component {
get canCopySelection() {
return this.selectionList.availableModels.length === 1;
}

// site navigation --------------------------------------------------------

// a nav link points at the page's published URL, so only published pages can
// be placed in navigation - linking a draft/scheduled page would 404. Draft
// pages set their placement at publish time via the publish flow instead.
get canManageNavigation() {
return this.type === 'page'
&& this.session.user.isAdmin
&& this.selectionList.availableModels.every(model => model.status === 'published');
}

get selectedPagePlacements() {
return this.selectionList.availableModels
.map(model => getPagePlacement(this.settings, pagePathForSlug(model.slug, this.config.blogUrl), this.config.blogUrl));
}

get isSingleSelection() {
return this.selectionList.availableModels.length === 1;
}

// current placement when exactly one page is selected, otherwise null
get singleNavigationPlacement() {
return this.isSingleSelection ? this.selectedPagePlacements[0] : null;
}

// only offer a destination the selection isn't already entirely in
get canAddToPrimaryNavigation() {
return this.selectedPagePlacements.some(placement => placement !== 'primary');
}

get canAddToSecondaryNavigation() {
return this.selectedPagePlacements.some(placement => placement !== 'secondary');
}

get canRemoveFromNavigation() {
return this.selectedPagePlacements.some(placement => placement !== null);
}

// a single already-linked page is moved between menus rather than added
get primaryNavigationActionLabel() {
return this.singleNavigationPlacement === 'secondary' ? 'Move to primary navigation' : 'Add to primary navigation';
}

get secondaryNavigationActionLabel() {
return this.singleNavigationPlacement === 'primary' ? 'Move to secondary navigation' : 'Add to secondary navigation';
}

@action
addToPrimaryNavigation() {
this.menu.performTask({perform: () => this.updateNavigationPlacementTask.perform('primary')});
}

@action
addToSecondaryNavigation() {
this.menu.performTask({perform: () => this.updateNavigationPlacementTask.perform('secondary')});
}

@action
removeFromNavigation() {
this.menu.performTask({perform: () => this.updateNavigationPlacementTask.perform('none')});
}

@task
*updateNavigationPlacementTask(placement) {
const pages = this.selectionList.availableModels
.map(model => ({label: model.title, path: pagePathForSlug(model.slug, this.config.blogUrl)}));
const count = pages.length;
// a single already-linked page is moved rather than added
const isMove = count === 1 && this.singleNavigationPlacement && this.singleNavigationPlacement !== placement;

try {
yield setPagesNavigationPlacement(this.settings, {
pages,
placement: placement === 'none' ? null : placement,
blogUrl: this.config.blogUrl
});

let message;
if (placement === 'none') {
message = count > 1 ? `${count} pages removed from navigation` : 'Page removed from navigation';
} else if (isMove) {
message = `Page moved to ${placement} navigation`;
} else {
message = count > 1 ? `${count} pages added to ${placement} navigation` : `Page added to ${placement} navigation`;
}

this.notifications.showNotification(message, {type: 'success'});
} catch (error) {
this.notifications.showAPIError(error, {key: 'navigation.save'});
}

return true;
}
}
6 changes: 6 additions & 0 deletions ghost/admin/app/components/posts-list/list-item-analytics.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@
{{/if}}
</span>
{{/if}}

{{#if this.navigationPlacement}}
<span class="gh-content-entry-nav" title="In {{this.navigationPlacement}} navigation" data-test-nav-indicator={{this.navigationPlacement}}>
{{svg-jar "navigation"}} In {{this.navigationPlacement}} navigation
</span>
{{/if}}
</p>
{{/unless}}
</div>
Expand Down
13 changes: 13 additions & 0 deletions ghost/admin/app/components/posts-list/list-item-analytics.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatPostTime} from 'ghost-admin/helpers/gh-format-post-time';
import {getPagePlacement, pagePathForSlug} from 'ghost-admin/utils/site-navigation';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
Expand Down Expand Up @@ -28,6 +29,18 @@ export default class PostsListItemClicks extends Component {
return '';
}

// 'primary' / 'secondary' when this page is linked in the site navigation,
// otherwise null. A nav link points at the published URL, so placement is
// only surfaced for live pages - a draft's link would 404 and shouldn't
// read as "in navigation".
get navigationPlacement() {
if (!this.post.isPage || !this.post.isPublished) {
return null;
}

return getPagePlacement(this.settings, pagePathForSlug(this.post.slug, this.config.blogUrl), this.config.blogUrl);
}

get scheduledText() {
let text = [];

Expand Down
5 changes: 4 additions & 1 deletion ghost/admin/app/styles/components/dropdowns.css
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ Post context menu
}

.gh-posts-context-menu {
max-width: 160px !important;
width: max-content !important;
min-width: 160px !important;
max-width: 260px !important;
border-radius: var(--border-radius);
box-shadow:
0 0 2.3px rgba(0, 0, 0, 0.028),
Expand All @@ -375,6 +377,7 @@ Post context menu
.gh-posts-context-menu li > button span {
display: flex;
align-items: center;
white-space: nowrap;
}

.gh-posts-context-menu li > button span svg,
Expand Down
21 changes: 21 additions & 0 deletions ghost/admin/app/styles/layouts/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,27 @@
font-weight: 500;
}

.gh-content-entry-nav {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid var(--whitegrey);
color: var(--midgrey);
font-weight: 500;
text-transform: capitalize;
}

.gh-content-entry-nav svg {
width: 11px;
height: 11px;
}

.gh-content-entry-nav svg path {
stroke: currentcolor;
}

.schedule-details {
margin-left: 3px;
color: var(--midlightgrey-d1);
Expand Down
Loading
Loading