Skip to content

Commit b64511a

Browse files
committed
Merge pull request #1839 from hswolff/1351-post-settings-ux
New Post UX behaviour.
2 parents d5b57a9 + a1f64d2 commit b64511a

File tree

10 files changed

+172
-74
lines changed

10 files changed

+172
-74
lines changed

core/client/assets/sass/layouts/editor.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,20 @@ body.zen {
634634
position: relative;
635635
padding: 0;
636636
z-index: 1;
637+
638+
&.unsaved {
639+
.post-settings-menu {
640+
padding-bottom: 0;
641+
642+
.post-setting:nth-child(3) td {
643+
border-bottom: none;
644+
}
645+
646+
.delete {
647+
display: none;
648+
}
649+
}
650+
}
637651
}
638652

639653
#entry-actions {

core/client/views/editor.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,9 @@
396396
if (rawTitle !== trimmedTitle) {
397397
$title.val(trimmedTitle);
398398
}
399+
400+
// Trigger title change for post-settings.js
401+
this.model.set('title', trimmedTitle);
399402
},
400403

401404
renderTitle: function () {

core/client/views/post-settings.js

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@
2121
this.listenTo(this.model, 'change:status', this.render);
2222
this.listenTo(this.model, 'change:published_at', this.render);
2323
this.listenTo(this.model, 'change:page', this.render);
24+
this.listenTo(this.model, 'change:title', this.updateSlugPlaceholder);
2425
}
2526
},
2627

2728
render: function () {
2829
var slug = this.model ? this.model.get('slug') : '',
2930
pubDate = this.model ? this.model.get('published_at') : 'Not Published',
30-
$pubDateEl = this.$('.post-setting-date');
31+
$pubDateEl = this.$('.post-setting-date'),
32+
$postSettingSlugEl = this.$('.post-setting-slug');
3133

32-
$('.post-setting-slug').val(slug);
34+
$postSettingSlugEl.val(slug);
3335

3436
// Update page status test if already a page.
3537
if (this.model && this.model.get('page')) {
@@ -46,9 +48,42 @@
4648
this.$('.delete').removeClass('hidden');
4749
}
4850

51+
// Apply different style for model's that aren't
52+
// yet persisted to the server.
53+
// Mostly we're hiding the delete post UI
54+
if (this.model.id === undefined) {
55+
this.$el.addClass('unsaved');
56+
} else {
57+
this.$el.removeClass('unsaved');
58+
}
59+
4960
$pubDateEl.val(pubDate);
5061
},
5162

63+
// Requests a new slug when the title was changed
64+
updateSlugPlaceholder: function () {
65+
var title = this.model.get('title'),
66+
$postSettingSlugEl = this.$('.post-setting-slug');
67+
68+
// If there's a title present we want to
69+
// validate it against existing slugs in the db
70+
// and then update the placeholder value.
71+
if (title) {
72+
$.ajax({
73+
url: Ghost.paths.apiRoot + '/posts/getSlug/' + encodeURIComponent(title) + '/',
74+
success: function (result) {
75+
$postSettingSlugEl.attr('placeholder', result);
76+
}
77+
});
78+
} else {
79+
// If there's no title set placeholder to blank
80+
// and don't make an ajax request to server
81+
// for a proper slug (as there won't be any).
82+
$postSettingSlugEl.attr('placeholder', '');
83+
return;
84+
}
85+
},
86+
5287
selectSlug: function (e) {
5388
e.currentTarget.select();
5489
},
@@ -60,8 +95,18 @@
6095
slugEl = e.currentTarget,
6196
newSlug = slugEl.value;
6297

63-
// Ignore empty or unchanged slugs
64-
if (newSlug.length === 0 || slug === newSlug) {
98+
// If the model doesn't currently
99+
// exist on the server (aka has no id)
100+
// then just update the model's value
101+
if (self.model.id === undefined) {
102+
this.model.set({
103+
slug: newSlug
104+
});
105+
return;
106+
}
107+
108+
// Ignore unchanged slugs
109+
if (slug === newSlug) {
65110
slugEl.value = slug === undefined ? '' : slug;
66111
return;
67112
}
@@ -102,7 +147,7 @@
102147
pubDateMoment,
103148
newPubDateMoment;
104149

105-
// Ignore empty or unchanged dates
150+
// if there is no new pub date do nothing
106151
if (!newPubDate) {
107152
return;
108153
}
@@ -155,6 +200,16 @@
155200
return;
156201
}
157202

203+
// If the model doesn't currently
204+
// exist on the server (aka has no id)
205+
// then just update the model's value
206+
if (self.model.id === undefined) {
207+
this.model.set({
208+
published_at: newPubDateMoment.toDate()
209+
});
210+
return;
211+
}
212+
158213
// Save new 'Published' date
159214
this.model.save({
160215
published_at: newPubDateMoment.toDate()
@@ -183,6 +238,16 @@
183238
var pageEl = $(e.currentTarget),
184239
page = pageEl.prop('checked');
185240

241+
// Don't try to save
242+
// if the model doesn't currently
243+
// exist on the server
244+
if (this.model.id === undefined) {
245+
this.model.set({
246+
page: page
247+
});
248+
return;
249+
}
250+
186251
this.model.save({
187252
page: page
188253
}, {
@@ -209,6 +274,11 @@
209274
deletePost: function (e) {
210275
e.preventDefault();
211276
var self = this;
277+
// You can't delete a post
278+
// that hasn't yet been saved
279+
if (this.model.id === undefined) {
280+
return;
281+
}
212282
this.addSubview(new Ghost.Views.Modal({
213283
model: {
214284
options: {

core/server/api/posts.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ posts = {
4848
});
4949
},
5050

51+
getSlug: function getSlug(args) {
52+
return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) {
53+
if (slug) {
54+
return slug;
55+
}
56+
return when.reject({errorCode: 500, message: 'Could not generate slug'});
57+
});
58+
},
59+
5160
// #### Edit
5261

5362
// **takes:** a json object with all the properties which should be updated

core/server/models/base.js

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -93,67 +93,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
9393

9494
sanitize: function (attr) {
9595
return sanitize(this.get(attr)).xss();
96-
},
97-
98-
// #### generateSlug
99-
// Create a string act as the permalink for an object.
100-
generateSlug: function (Model, base, readOptions) {
101-
var slug,
102-
slugTryCount = 1,
103-
// Look for a post with a matching slug, append an incrementing number if so
104-
checkIfSlugExists = function (slugToFind) {
105-
var args = {slug: slugToFind};
106-
//status is needed for posts
107-
if (readOptions && readOptions.status) {
108-
args.status = readOptions.status;
109-
}
110-
return Model.findOne(args, readOptions).then(function (found) {
111-
var trimSpace;
112-
113-
if (!found) {
114-
return when.resolve(slugToFind);
115-
}
116-
117-
slugTryCount += 1;
118-
119-
// If this is the first time through, add the hyphen
120-
if (slugTryCount === 2) {
121-
slugToFind += '-';
122-
} else {
123-
// Otherwise, trim the number off the end
124-
trimSpace = -(String(slugTryCount - 1).length);
125-
slugToFind = slugToFind.slice(0, trimSpace);
126-
}
127-
128-
slugToFind += slugTryCount;
129-
130-
return checkIfSlugExists(slugToFind);
131-
});
132-
};
133-
134-
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
135-
slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
136-
// Replace dots and spaces with a dash
137-
.replace(/(\s|\.)/g, '-')
138-
// Convert 2 or more dashes into a single dash
139-
.replace(/-+/g, '-')
140-
// Make the whole thing lowercase
141-
.toLowerCase();
142-
143-
// Remove trailing hyphen
144-
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
145-
// Remove non ascii characters
146-
slug = unidecode(slug);
147-
// Check the filtered slug doesn't match any of the reserved keywords
148-
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g
149-
.test(slug) ? slug + '-post' : slug;
150-
151-
//if slug is empty after trimming use "post"
152-
if (!slug) {
153-
slug = 'post';
154-
}
155-
// Test for duplicate slugs.
156-
return checkIfSlugExists(slug);
15796
}
15897

15998
}, {
@@ -236,6 +175,67 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
236175

237176
'delete': function () {
238177
return this.destroy.apply(this, arguments);
178+
},
179+
180+
// #### generateSlug
181+
// Create a string act as the permalink for an object.
182+
generateSlug: function (Model, base, readOptions) {
183+
var slug,
184+
slugTryCount = 1,
185+
// Look for a post with a matching slug, append an incrementing number if so
186+
checkIfSlugExists = function (slugToFind) {
187+
var args = {slug: slugToFind};
188+
//status is needed for posts
189+
if (readOptions && readOptions.status) {
190+
args.status = readOptions.status;
191+
}
192+
return Model.findOne(args, readOptions).then(function (found) {
193+
var trimSpace;
194+
195+
if (!found) {
196+
return when.resolve(slugToFind);
197+
}
198+
199+
slugTryCount += 1;
200+
201+
// If this is the first time through, add the hyphen
202+
if (slugTryCount === 2) {
203+
slugToFind += '-';
204+
} else {
205+
// Otherwise, trim the number off the end
206+
trimSpace = -(String(slugTryCount - 1).length);
207+
slugToFind = slugToFind.slice(0, trimSpace);
208+
}
209+
210+
slugToFind += slugTryCount;
211+
212+
return checkIfSlugExists(slugToFind);
213+
});
214+
};
215+
216+
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
217+
slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
218+
// Replace dots and spaces with a dash
219+
.replace(/(\s|\.)/g, '-')
220+
// Convert 2 or more dashes into a single dash
221+
.replace(/-+/g, '-')
222+
// Make the whole thing lowercase
223+
.toLowerCase();
224+
225+
// Remove trailing hyphen
226+
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
227+
// Remove non ascii characters
228+
slug = unidecode(slug);
229+
// Check the filtered slug doesn't match any of the reserved keywords
230+
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g
231+
.test(slug) ? slug + '-post' : slug;
232+
233+
//if slug is empty after trimming use "post"
234+
if (!slug) {
235+
slug = 'post';
236+
}
237+
// Test for duplicate slugs.
238+
return checkIfSlugExists(slug);
239239
}
240240

241241
});

core/server/models/post.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Post = ghostBookshelf.Model.extend({
6767

6868
if (this.hasChanged('slug')) {
6969
// Pass the new slug through the generator to strip illegal characters, detect duplicates
70-
return this.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting})
70+
return ghostBookshelf.Model.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting})
7171
.then(function (slug) {
7272
self.set({slug: slug});
7373
});
@@ -85,9 +85,11 @@ Post = ghostBookshelf.Model.extend({
8585

8686
ghostBookshelf.Model.prototype.creating.call(this);
8787

88+
// We require a slug be set when creating a new post
89+
// as the database doesn't allow null slug values.
8890
if (!this.get('slug')) {
8991
// Generating a slug requires a db call to look for conflicting slugs
90-
return this.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting})
92+
return ghostBookshelf.Model.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting})
9193
.then(function (slug) {
9294
self.set({slug: slug});
9395
});
@@ -398,7 +400,6 @@ Post = ghostBookshelf.Model.extend({
398400
return post.destroy(options);
399401
});
400402
}
401-
402403
});
403404

404405
Posts = ghostBookshelf.Collection.extend({

core/server/models/tag.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Tag = ghostBookshelf.Model.extend({
2424

2525
if (!this.get('slug')) {
2626
// Generating a slug requires a db call to look for conflicting slugs
27-
return this.generateSlug(Tag, this.get('name'))
27+
return ghostBookshelf.Model.generateSlug(Tag, this.get('name'))
2828
.then(function (slug) {
2929
self.set({slug: slug});
3030
});

core/server/models/user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ User = ghostBookshelf.Model.extend({
5858

5959
if (!this.get('slug')) {
6060
// Generating a slug requires a db call to look for conflicting slugs
61-
return this.generateSlug(User, this.get('name'))
61+
return ghostBookshelf.Model.generateSlug(User, this.get('name'))
6262
.then(function (slug) {
6363
self.set({slug: slug});
6464
});

core/server/routes/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = function (server) {
1010
server.get('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.read));
1111
server.put('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.edit));
1212
server.del('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.destroy));
13+
server.get('/ghost/api/v0.1/posts/getSlug/:title', middleware.authAPI, api.requestHandler(api.posts.getSlug));
1314
// #### Settings
1415
server.get('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.browse));
1516
server.get('/ghost/api/v0.1/settings/:key/', middleware.authAPI, api.requestHandler(api.settings.read));

0 commit comments

Comments
 (0)