Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Custom Post Templates UI
Browse files Browse the repository at this point in the history
refs TryGhost/Ghost#9060, requires TryGhost/Ghost#9073
- add `{{gh-psm-template-select}}` component
  - allows selection of a custom template for a post if the active theme has custom templates
  - loads themes on render, only hitting the server if not already in the store
  - disables select if post slug matches a `post-*.hbs` or `page-*.hbs` template
- adds `customTemplate` attr to `Post` model
- adds `templates` attr to `Theme` model with CPs to pull out custom vs post/page override templates
- add `.gh-select.disabled` styles to make disabled selects look visually disabled
  • Loading branch information
kevinansfield committed Oct 10, 2017
1 parent dd307ee commit 5694dc5
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 7 deletions.
75 changes: 75 additions & 0 deletions app/components/gh-psm-template-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Component from '@ember/component';
import {computed} from '@ember/object';
import {inject as injectService} from '@ember/service';
import {isEmpty} from '@ember/utils';
import {task} from 'ember-concurrency';

export default Component.extend({

store: injectService(),

// public attributes
tagName: '',
post: null,

// internal properties
activeTheme: null,

// closure actions
onTemplateSelect() {},

// computed properties
customTemplates: computed('activeTheme.customTemplates.[]', function () {
let templates = this.get('activeTheme.customTemplates') || [];
let defaultTemplate = {
filename: '',
name: 'Default'
};

return isEmpty(templates) ? templates : [defaultTemplate, ...templates.sortBy('name')];
}),

matchedSlugTemplate: computed('post.{page,slug}', 'activeTheme.slugTemplates.[]', function () {
let slug = this.get('post.slug');
let type = this.get('post.page') ? 'page' : 'post';

let [matchedTemplate] = this.get('activeTheme.slugTemplates').filter(function (template) {
return template.for.includes(type) && template.slug === slug;
});

return matchedTemplate;
}),

selectedTemplate: computed('post.customTemplate', 'customTemplates.[]', function () {
let templates = this.get('customTemplates');
let filename = this.get('post.customTemplate');

return templates.findBy('filename', filename);
}),

// hooks
didInsertElement() {
this._super(...arguments);
this.get('loadActiveTheme').perform();
},

// tasks
loadActiveTheme: task(function* () {
let store = this.get('store');
let themes = yield store.peekAll('theme');

if (isEmpty(themes)) {
themes = yield store.findAll('theme');
}

let activeTheme = themes.filterBy('active', true).get('firstObject');

this.set('activeTheme', activeTheme);
}),

actions: {
selectTemplate(template) {
this.onTemplateSelect(template.filename);
}
}
});
1 change: 1 addition & 0 deletions app/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default Model.extend(Comparable, ValidationEngine, {
featureImage: attr('string'),
codeinjectionFoot: attr('string', {defaultValue: ''}),
codeinjectionHead: attr('string', {defaultValue: ''}),
customTemplate: attr('string'),
ogImage: attr('string'),
ogTitle: attr('string'),
ogDescription: attr('string'),
Expand Down
8 changes: 5 additions & 3 deletions app/models/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default Model.extend(ValidationEngine, {
password: attr('string'),
slack: attr('slack-settings'),
amp: attr('boolean'),
unsplash: attr('unsplash-settings', {defaultValue() {
return {isActive: true};
}})
unsplash: attr('unsplash-settings', {
defaultValue() {
return {isActive: true};
}
})
});
23 changes: 21 additions & 2 deletions app/models/theme.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import {computed} from '@ember/object';
import {isBlank} from '@ember/utils';

export default Model.extend({
active: attr('boolean'),
errors: attr('raw'),
name: attr('string'),
package: attr('raw'),
active: attr('boolean'),
templates: attr('raw', {defaultValue: () => []}),
warnings: attr('raw'),
errors: attr('raw'),

customTemplates: computed('templates.[]', function () {
let templates = this.get('templates') || [];

return templates.filter(function (template) {
return isBlank(template.slug);
});
}),

slugTemplates: computed('templates.[]', function () {
let templates = this.get('templates') || [];

return templates.filter(function (template) {
return !isBlank(template.slug);
});
}),

activate() {
let adapter = this.store.adapterFor(this.constructor.modelName);
Expand Down
9 changes: 9 additions & 0 deletions app/styles/patterns/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,15 @@ textarea {
text-shadow: 0 0 0 #000;
}

.gh-select.disabled select {
color: color(var(--midgrey) l(+18%));
cursor: default;
}

.gh-select.disabled svg path {
stroke: color(var(--midgrey) l(+30%));
}


/* File Uploads
/* ---------------------------------------------------------- */
Expand Down
18 changes: 16 additions & 2 deletions app/templates/components/gh-post-settings-menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,32 @@

<div class="form-group for-checkbox">
<label class="checkbox" for="static-page" {{action "togglePage" bubbles="false"}}>
{{one-way-checkbox model.page type="checkbox" name="static-page" id="static-page" class="gh-input post-setting-static-page" update=(action (mut model.page))}}
{{one-way-checkbox model.page
name="static-page"
id="static-page"
class="gh-input post-setting-static-page"
update=(action (mut model.page))
}}
<span class="input-toggle-component"></span>
<p>Turn this post into a page</p>
</label>

<label class="checkbox" for="featured" {{action "toggleFeatured" bubbles="false"}}>
{{one-way-checkbox model.featured type="checkbox" name="featured" id="featured" class="gh-input post-setting-featured" update=(action (mut model.featured))}}
{{one-way-checkbox model.featured
name="featured"
id="featured"
class="gh-input post-setting-featured"
update=(action (mut model.featured))
}}
<span class="input-toggle-component"></span>
<p>Feature this post</p>
</label>
</div>

{{gh-psm-template-select
post=model
onTemplateSelect=(action (mut model.customTemplate))}}

{{#unless model.isNew}}
<button type="button" class="gh-btn gh-btn-link gh-btn-icon settings-menu-delete-button" {{action "deletePost"}}><span>{{inline-svg "trash"}} Delete Post</span></button>
{{/unless}}
Expand Down
19 changes: 19 additions & 0 deletions app/templates/components/gh-psm-template-select.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{#if customTemplates}}
<div class="form-group for-select" data-test-custom-template-form>
<label for="author-list">Template</label>
<span class="gh-input-icon gh-icon-user">
{{inline-svg "file-text-document"}}
<span class="gh-select {{if matchedSlugTemplate "disabled"}}">
{{one-way-select selectedTemplate
options=customTemplates
optionValuePath="filename"
optionLabelPath="name"
update=(action "selectTemplate")
disabled=matchedSlugTemplate
data-test-select="custom-template"}}
{{inline-svg "arrow-down-small"}}
</span>
</span>
<p>Post URL matches {{matchedSlugTemplate.filename}}</p>
</div>
{{/if}}
151 changes: 151 additions & 0 deletions tests/acceptance/custom-post-templates-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from 'ghost-admin/tests/helpers/destroy-app';
import startApp from 'ghost-admin/tests/helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {click, fillIn, find, keyEvent, visit} from 'ember-native-dom-helpers';
import {expect} from 'chai';

// keyCodes
const KEY_S = 83;

describe('Acceptance: Custom Post Templates', function() {
let application;

beforeEach(function() {
application = startApp();

server.loadFixtures('settings');

let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});

authenticateSession(application);
});

afterEach(function() {
destroyApp(application);
});

describe('with custom templates', function () {
beforeEach(function () {
server.create('theme', {
active: true,
name: 'example-theme',
package: {
name: 'Example Theme',
version: '0.1'
},
templates: [
{
filename: 'custom-news-bulletin.hbs',
name: 'News Bulletin',
for: ['post', 'page'],
slug: null
},
{
filename: 'custom-big-images.hbs',
name: 'Big Images',
for: ['post', 'page'],
slug: null
},
{
filename: 'post-one.hbs',
name: 'One',
for: ['post'],
slug: 'one'
},
{
filename: 'page-about.hbs',
name: 'About',
for: ['page'],
slug: 'about'
}
]
});
});

it('can change selected template', async function () {
let post = server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});

await visit('/editor/1');
await click('[data-test-psm-trigger]');

// template form should be shown
expect(find('[data-test-custom-template-form]')).to.exist;

// custom template should be selected
let select = find('[data-test-select="custom-template"]');
expect(select.value, 'selected value').to.equal('custom-news-bulletin.hbs');

// templates list should contain default and custom templates in alphabetical order
expect(select.options.length).to.equal(3);
expect(select.options.item(0).value, 'default value').to.equal('');
expect(select.options.item(0).text, 'default text').to.equal('Default');
expect(select.options.item(1).value, 'first custom value').to.equal('custom-big-images.hbs');
expect(select.options.item(1).text, 'first custom text').to.equal('Big Images');
expect(select.options.item(2).value, 'second custom value').to.equal('custom-news-bulletin.hbs');
expect(select.options.item(2).text, 'second custom text').to.equal('News Bulletin');

// select the default template
await fillIn(select, '');

// save then check server record
await keyEvent('.gh-app', 'keydown', KEY_S, {
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
});

expect(
server.db.posts.find(post.id).customTemplate,
'saved custom template'
).to.equal('');
});

it('disables template selector if slug matches slug-based template');

it('doesn\'t query themes endpoint unncessarily', async function () {
function themeRequests() {
return server.pretender.handledRequests.filter(function (request) {
return request.url.match(/\/themes\//);
});
}

server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});

await visit('/editor/1');
await click('[data-test-psm-trigger]');

expect(themeRequests().length, 'after first open').to.equal(1);

await click('[data-test-psm-trigger]'); // hide
await click('[data-test-psm-trigger]'); // show

expect(themeRequests().length, 'after second open').to.equal(1);
});
});

describe('without custom templates', function () {
beforeEach(function () {
server.create('theme', {
active: true,
name: 'example-theme',
package: {
name: 'Example Theme',
version: '0.1'
},
templates: []
});
});

it('doesn\'t show template selector', async function () {
server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});

await visit('/editor/1');
await click('[data-test-psm-trigger]');

// template form should be shown
expect(find('[data-test-custom-template-form]')).to.not.exist;
});
});
});

0 comments on commit 5694dc5

Please sign in to comment.