mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 22:02:11 +03:00
3656297843
no issue - fixes bug where multiple posts were created starting with one char and growing until the new->edit transition completed, e.g. posts with content such as `a`, `ab`, `abcd` were created in quick succession - moves old `_savePromise` body into the `save` task - call the `save` task instead of the old `_savePromise` so that concurrency is handled properly - fixes odd behaviour with the "Are you sure you want to leave?" modal appearing too often - it's now aware of on-going or scheduled saves and will wait for those to complete before transitioning - move all transition abort/save/retry handling into `toggleLeaveEditorModal` method - check for a running save, wait for it to finish then retry the transition - check for a scheduled autosave, cancel it if present and perform an immediate autosave then retry the transition - don't attempt new->edit transition on successful save of new post if we're already waiting for a different transition - once the new->edit transition has completed, if the post body content has changed schedule an autosave manually so that the user doesn't need to type something again to save what they assume is already saved - remove debounced slug generation/save on type of title field in favour of generation and save on focus out which plays a lot nicer with the new transition autosave behaviour
676 lines
23 KiB
JavaScript
676 lines
23 KiB
JavaScript
import Ember from 'ember';
|
|
import Mixin from 'ember-metal/mixin';
|
|
import PostModel from 'ghost-admin/models/post';
|
|
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
|
import computed, {mapBy, reads} from 'ember-computed';
|
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
import injectController from 'ember-controller/inject';
|
|
import injectService from 'ember-service/inject';
|
|
import isNumber from 'ghost-admin/utils/isNumber';
|
|
import moment from 'moment';
|
|
import {htmlSafe} from 'ember-string';
|
|
import {isBlank} from 'ember-utils';
|
|
import {isEmberArray} from 'ember-array/utils';
|
|
import {isInvalidError} from 'ember-ajax/errors';
|
|
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
|
import {task, taskGroup, timeout} from 'ember-concurrency';
|
|
|
|
// ember-cli-shims doesn't export Ember.testing
|
|
const {testing} = Ember;
|
|
|
|
// this array will hold properties we need to watch
|
|
// to know if the model has been changed (`controller.hasDirtyAttributes`)
|
|
const watchedProps = ['model.scratch', 'model.titleScratch', 'model.hasDirtyAttributes', 'model.tags.[]'];
|
|
|
|
const DEFAULT_TITLE = '(Untitled)';
|
|
|
|
// time in ms to save after last content edit
|
|
const AUTOSAVE_TIMEOUT = 3000;
|
|
// time in ms to force a save if the user is continuously typing
|
|
const TIMEDSAVE_TIMEOUT = 60000;
|
|
|
|
PostModel.eachAttribute(function (name) {
|
|
watchedProps.push(`model.${name}`);
|
|
});
|
|
|
|
export default Mixin.create({
|
|
|
|
showLeaveEditorModal: false,
|
|
showReAuthenticateModal: false,
|
|
showDeletePostModal: false,
|
|
shouldFocusEditor: true,
|
|
|
|
application: injectController(),
|
|
notifications: injectService(),
|
|
clock: injectService(),
|
|
slugGenerator: injectService(),
|
|
|
|
wordcount: 0,
|
|
cards: [], // for apps
|
|
atoms: [], // for apps
|
|
toolbar: [], // for apps
|
|
apiRoot: ghostPaths().apiRoot,
|
|
assetPath: ghostPaths().assetRoot,
|
|
editor: null,
|
|
editorMenuIsOpen: false,
|
|
|
|
navIsClosed: reads('application.autoNav'),
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
window.onbeforeunload = () => {
|
|
return this.get('hasDirtyAttributes') ? this.unloadDirtyMessage() : null;
|
|
};
|
|
},
|
|
|
|
_canAutosave: computed('model.isDraft', function () {
|
|
return !testing && this.get('model.isDraft');
|
|
}),
|
|
|
|
// save 3 seconds after the last edit
|
|
_autosave: task(function* () {
|
|
// force an instant save on first body edit for new posts
|
|
if (this.get('_canAutosave') && this.get('model.isNew')) {
|
|
return this.get('autosave').perform();
|
|
}
|
|
|
|
yield timeout(AUTOSAVE_TIMEOUT);
|
|
|
|
if (this.get('_canAutosave')) {
|
|
this.get('autosave').perform();
|
|
}
|
|
}).restartable(),
|
|
|
|
// save at 60 seconds even if the user doesn't stop typing
|
|
_timedSave: task(function* () {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (!testing && true) {
|
|
yield timeout(TIMEDSAVE_TIMEOUT);
|
|
|
|
if (this.get('_canAutosave')) {
|
|
this.get('autosave').perform();
|
|
}
|
|
}
|
|
}).drop(),
|
|
|
|
// separate task for autosave so that it doesn't override a manual save
|
|
autosave: task(function* () {
|
|
if (!this.get('save.isRunning')) {
|
|
return yield this.get('save').perform({
|
|
silent: true,
|
|
backgroundSave: true
|
|
});
|
|
}
|
|
}).drop(),
|
|
|
|
_autosaveRunning: computed('_autosave.isRunning', '_timedSave.isRunning', function () {
|
|
let autosave = this.get('_autosave.isRunning');
|
|
let timedsave = this.get('_timedSave.isRunning');
|
|
|
|
return autosave || timedsave;
|
|
}),
|
|
|
|
// updateSlug and save should always be enqueued so that we don't run into
|
|
// problems with concurrency, for example when Cmd-S is pressed whilst the
|
|
// cursor is in the slug field - that would previously trigger a simultaneous
|
|
// slug update and save resulting in ember data errors and inconsistent save
|
|
// results
|
|
saveTasks: taskGroup().enqueue(),
|
|
|
|
// save tasks cancels autosave before running, although this cancels the
|
|
// _xSave tasks that will also cancel the autosave task
|
|
save: task(function* (options = {}) {
|
|
let prevStatus = this.get('model.status');
|
|
let isNew = this.get('model.isNew');
|
|
let status;
|
|
|
|
this.send('cancelAutosave');
|
|
|
|
if (options.backgroundSave && !this.get('hasDirtyAttributes')) {
|
|
return;
|
|
}
|
|
|
|
if (options.backgroundSave) {
|
|
// do not allow a post's status to be set to published by a background save
|
|
status = 'draft';
|
|
} else {
|
|
if (this.get('post.pastScheduledTime')) {
|
|
status = (!this.get('willSchedule') && !this.get('willPublish')) ? 'draft' : 'published';
|
|
} else {
|
|
if (this.get('willPublish') && !this.get('model.isScheduled') && !this.get('statusFreeze')) {
|
|
status = 'published';
|
|
} else if (this.get('willSchedule') && !this.get('model.isPublished') && !this.get('statusFreeze')) {
|
|
status = 'scheduled';
|
|
} else {
|
|
status = 'draft';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the properties that are indirected
|
|
// set mobiledoc equal to what's in the editor, minus the image markers.
|
|
this.set('model.mobiledoc', this.get('model.scratch'));
|
|
this.set('model.status', status);
|
|
|
|
// Set a default title
|
|
if (!this.get('model.titleScratch').trim()) {
|
|
this.set('model.titleScratch', DEFAULT_TITLE);
|
|
}
|
|
|
|
this.set('model.title', this.get('model.titleScratch'));
|
|
this.set('model.metaTitle', this.get('model.metaTitleScratch'));
|
|
this.set('model.metaDescription', this.get('model.metaDescriptionScratch'));
|
|
|
|
if (!this.get('model.slug')) {
|
|
this.get('saveTitle').cancelAll();
|
|
|
|
yield this.get('generateSlug').perform();
|
|
}
|
|
|
|
try {
|
|
let model = yield this.get('model').save(options);
|
|
|
|
if (!options.silent) {
|
|
this.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false);
|
|
}
|
|
|
|
this.get('model').set('statusScratch', null);
|
|
|
|
// redirect to edit route if saving a new record
|
|
if (isNew && model.get('id')) {
|
|
if (!this.get('leaveEditorTransition')) {
|
|
this.replaceRoute('editor.edit', model);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return model;
|
|
|
|
} catch (error) {
|
|
// re-throw if we have a general server error
|
|
if (error && !isInvalidError(error)) {
|
|
this.send('error', error);
|
|
return;
|
|
}
|
|
|
|
this.set('model.status', prevStatus);
|
|
|
|
if (!options.silent) {
|
|
let errorOrMessages = error || this.get('model.errors.messages');
|
|
this.showErrorAlert(prevStatus, this.get('model.status'), errorOrMessages);
|
|
// simulate a validation error for upstream tasks
|
|
throw undefined;
|
|
}
|
|
|
|
return this.get('model');
|
|
}
|
|
}).group('saveTasks'),
|
|
|
|
/*
|
|
* triggered by a user manually changing slug
|
|
*/
|
|
updateSlug: task(function* (_newSlug) {
|
|
let slug = this.get('model.slug');
|
|
let newSlug, serverSlug;
|
|
|
|
newSlug = _newSlug || slug;
|
|
newSlug = newSlug && newSlug.trim();
|
|
|
|
// Ignore unchanged slugs or candidate slugs that are empty
|
|
if (!newSlug || slug === newSlug) {
|
|
// reset the input to its previous state
|
|
this.set('slugValue', slug);
|
|
return;
|
|
}
|
|
|
|
serverSlug = yield this.get('slugGenerator').generateSlug('post', newSlug);
|
|
|
|
// If after getting the sanitized and unique slug back from the API
|
|
// we end up with a slug that matches the existing slug, abort the change
|
|
if (serverSlug === slug) {
|
|
return;
|
|
}
|
|
|
|
// Because the server transforms the candidate slug by stripping
|
|
// certain characters and appending a number onto the end of slugs
|
|
// to enforce uniqueness, there are cases where we can get back a
|
|
// candidate slug that is a duplicate of the original except for
|
|
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
|
|
|
|
// get the last token out of the slug candidate and see if it's a number
|
|
let slugTokens = serverSlug.split('-');
|
|
let check = Number(slugTokens.pop());
|
|
|
|
// if the candidate slug is the same as the existing slug except
|
|
// for the incrementor then the existing slug should be used
|
|
if (isNumber(check) && check > 0) {
|
|
if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
|
|
this.set('slugValue', slug);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.set('model.slug', serverSlug);
|
|
|
|
// If this is a new post. Don't save the model. Defer the save
|
|
// to the user pressing the save button
|
|
if (this.get('model.isNew')) {
|
|
return;
|
|
}
|
|
|
|
return yield this.get('model').save();
|
|
}).group('saveTasks'),
|
|
|
|
/**
|
|
* By default, a post will not change its publish state.
|
|
* Only with a user-set value (via setSaveType action)
|
|
* can the post's status change.
|
|
*/
|
|
willPublish: boundOneWay('model.isPublished'),
|
|
willSchedule: boundOneWay('model.isScheduled'),
|
|
|
|
// set by the editor route and `hasDirtyAttributes`. useful when checking
|
|
// whether the number of tags has changed for `hasDirtyAttributes`.
|
|
previousTagNames: null,
|
|
|
|
tagNames: mapBy('model.tags', 'name'),
|
|
|
|
postOrPage: computed('model.page', function () {
|
|
return this.get('model.page') ? 'Page' : 'Post';
|
|
}),
|
|
|
|
// countdown timer to show the time left until publish time for a scheduled post
|
|
// starts 15 minutes before scheduled time
|
|
scheduleCountdown: computed('model.{publishedAtUTC,isScheduled}', 'clock.second', function () {
|
|
let isScheduled = this.get('model.isScheduled');
|
|
let publishTime = this.get('model.publishedAtUTC') || moment.utc();
|
|
let timeUntilPublished = publishTime.diff(moment.utc(), 'minutes', true);
|
|
let isPublishedSoon = timeUntilPublished > 0 && timeUntilPublished < 15;
|
|
|
|
// force a recompute
|
|
this.get('clock.second');
|
|
|
|
if (isScheduled && isPublishedSoon) {
|
|
return moment(publishTime).fromNow();
|
|
} else {
|
|
return false;
|
|
}
|
|
}),
|
|
|
|
// compares previousTagNames to tagNames
|
|
tagNamesEqual() {
|
|
let tagNames = this.get('tagNames') || [];
|
|
let previousTagNames = this.get('previousTagNames') || [];
|
|
let hashCurrent,
|
|
hashPrevious;
|
|
|
|
// beware! even if they have the same length,
|
|
// that doesn't mean they're the same.
|
|
if (tagNames.length !== previousTagNames.length) {
|
|
return false;
|
|
}
|
|
|
|
// instead of comparing with slow, nested for loops,
|
|
// perform join on each array and compare the strings
|
|
hashCurrent = tagNames.join('');
|
|
hashPrevious = previousTagNames.join('');
|
|
|
|
return hashCurrent === hashPrevious;
|
|
},
|
|
|
|
// a hook created in editor-base-route's setupController
|
|
modelSaved() {
|
|
let model = this.get('model');
|
|
|
|
// safer to updateTags on save in one place
|
|
// rather than in all other places save is called
|
|
model.updateTags();
|
|
|
|
// set previousTagNames to current tagNames for hasDirtyAttributes check
|
|
this.set('previousTagNames', this.get('tagNames'));
|
|
|
|
// `updateTags` triggers `hasDirtyAttributes => true`.
|
|
// for a saved model it would otherwise be false.
|
|
|
|
// if the two "scratch" properties (title and content) match the model, then
|
|
// it's ok to set hasDirtyAttributes to false
|
|
if (model.get('titleScratch') === model.get('title')
|
|
&& JSON.stringify(model.get('scratch')) === JSON.stringify(model.get('mobiledoc'))) {
|
|
this.set('hasDirtyAttributes', false);
|
|
}
|
|
},
|
|
|
|
// an ugly hack, but necessary to watch all the model's properties
|
|
// and more, without having to be explicit and do it manually
|
|
hasDirtyAttributes: computed.apply(Ember, watchedProps.concat({
|
|
get() {
|
|
let model = this.get('model');
|
|
|
|
if (!model) {
|
|
return false;
|
|
}
|
|
|
|
let mobiledoc = JSON.stringify(model.get('mobiledoc'));
|
|
let scratch = JSON.stringify(model.get('scratch'));
|
|
let title = model.get('title');
|
|
let titleScratch = model.get('titleScratch');
|
|
let changedAttributes;
|
|
|
|
if (!this.tagNamesEqual()) {
|
|
return true;
|
|
}
|
|
|
|
if (titleScratch !== title) {
|
|
return true;
|
|
}
|
|
|
|
// since `scratch` is not model property, we need to check
|
|
// it explicitly against the model's mobiledoc attribute
|
|
if (mobiledoc !== scratch) {
|
|
return true;
|
|
}
|
|
|
|
// if the Adapter failed to save the model isError will be true
|
|
// and we should consider the model still dirty.
|
|
if (model.get('isError')) {
|
|
return true;
|
|
}
|
|
|
|
// models created on the client always return `hasDirtyAttributes: true`,
|
|
// so we need to see which properties have actually changed.
|
|
if (model.get('isNew')) {
|
|
changedAttributes = Object.keys(model.changedAttributes());
|
|
|
|
if (changedAttributes.length) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// even though we use the `scratch` prop to show edits,
|
|
// which does *not* change the model's `hasDirtyAttributes` property,
|
|
// `hasDirtyAttributes` will tell us if the other props have changed,
|
|
// as long as the model is not new (model.isNew === false).
|
|
return model.get('hasDirtyAttributes');
|
|
},
|
|
set(key, value) {
|
|
return value;
|
|
}
|
|
})),
|
|
|
|
// used on window.onbeforeunload
|
|
unloadDirtyMessage() {
|
|
return '==============================\n\n'
|
|
+ 'Hey there! It looks like you\'re in the middle of writing'
|
|
+ ' something and you haven\'t saved all of your content.'
|
|
+ '\n\nSave before you go!\n\n'
|
|
+ '==============================';
|
|
},
|
|
|
|
// TODO: This has to be moved to the I18n localization file.
|
|
// This structure is supposed to be close to the i18n-localization which will be used soon.
|
|
messageMap: {
|
|
errors: {
|
|
post: {
|
|
published: {
|
|
published: 'Update failed',
|
|
draft: 'Saving failed',
|
|
scheduled: 'Scheduling failed'
|
|
},
|
|
draft: {
|
|
published: 'Publish failed',
|
|
draft: 'Saving failed',
|
|
scheduled: 'Scheduling failed'
|
|
},
|
|
scheduled: {
|
|
scheduled: 'Updated failed',
|
|
draft: 'Unscheduling failed',
|
|
published: 'Publish failed'
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
success: {
|
|
post: {
|
|
published: {
|
|
published: 'Updated.',
|
|
draft: 'Saved.',
|
|
scheduled: 'Scheduled.'
|
|
},
|
|
draft: {
|
|
published: 'Published!',
|
|
draft: 'Saved.',
|
|
scheduled: 'Scheduled.'
|
|
},
|
|
scheduled: {
|
|
scheduled: 'Updated.',
|
|
draft: 'Unscheduled.',
|
|
published: 'Published!'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// TODO: Update for new notification click-action API
|
|
showSaveNotification(prevStatus, status, delay) {
|
|
let message = this.messageMap.success.post[prevStatus][status];
|
|
let notifications = this.get('notifications');
|
|
let type, path;
|
|
|
|
if (status === 'published') {
|
|
type = this.get('postOrPage');
|
|
path = this.get('model.absoluteUrl');
|
|
} else {
|
|
type = 'Preview';
|
|
path = this.get('model.previewUrl');
|
|
}
|
|
|
|
message += ` <a href="${path}" target="_blank">View ${type}</a>`;
|
|
|
|
notifications.showNotification(message.htmlSafe(), {delayed: delay});
|
|
},
|
|
|
|
showErrorAlert(prevStatus, status, error, delay) {
|
|
let message = this.messageMap.errors.post[prevStatus][status];
|
|
let notifications = this.get('notifications');
|
|
let errorMessage;
|
|
|
|
function isString(str) {
|
|
/* global toString */
|
|
return toString.call(str) === '[object String]';
|
|
}
|
|
|
|
if (error && isString(error)) {
|
|
errorMessage = error;
|
|
} else if (error && isEmberArray(error)) {
|
|
// This is here because validation errors are returned as an array
|
|
// TODO: remove this once validations are fixed
|
|
errorMessage = error[0];
|
|
} else if (error && error.errors && error.errors[0].message) {
|
|
errorMessage = error.errors[0].message;
|
|
} else {
|
|
errorMessage = 'Unknown Error';
|
|
}
|
|
|
|
message += `: ${errorMessage}`;
|
|
message = htmlSafe(message);
|
|
|
|
notifications.showAlert(message, {type: 'error', delayed: delay, key: 'post.save'});
|
|
},
|
|
|
|
saveTitle: task(function* () {
|
|
let model = this.get('model');
|
|
let currentTitle = model.get('title');
|
|
let newTitle = model.get('titleScratch').trim();
|
|
|
|
if (currentTitle && newTitle && newTitle === currentTitle) {
|
|
return;
|
|
}
|
|
|
|
// this is necessary to force a save when the title is blank
|
|
this.set('hasDirtyAttributes', true);
|
|
|
|
// generate a slug if a post is new and doesn't have a title yet or
|
|
// if the title is still '(Untitled)'
|
|
if ((model.get('isNew') && !currentTitle) || currentTitle === DEFAULT_TITLE) {
|
|
yield this.get('generateSlug').perform();
|
|
}
|
|
|
|
if (this.get('model.isDraft')) {
|
|
yield this.get('autosave').perform();
|
|
}
|
|
}),
|
|
|
|
generateSlug: task(function* () {
|
|
let title = this.get('model.titleScratch');
|
|
|
|
// Only set an "untitled" slug once per post
|
|
if (title === DEFAULT_TITLE && this.get('model.slug')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let slug = yield this.get('slugGenerator').generateSlug('post', title);
|
|
|
|
if (!isBlank(slug)) {
|
|
this.set('model.slug', slug);
|
|
}
|
|
} catch (error) {
|
|
// Nothing to do (would be nice to log this somewhere though),
|
|
// but a rejected promise needs to be handled here so that a resolved
|
|
// promise is returned.
|
|
if (isVersionMismatchError(error)) {
|
|
this.get('notifications').showAPIError(error);
|
|
}
|
|
}
|
|
}).enqueue(),
|
|
|
|
actions: {
|
|
updateScratch(value) {
|
|
this.set('model.scratch', value);
|
|
|
|
// save 3 seconds after last edit
|
|
this.get('_autosave').perform();
|
|
// force save at 60 seconds
|
|
this.get('_timedSave').perform();
|
|
},
|
|
|
|
cancelAutosave() {
|
|
this.get('_autosave').cancelAll();
|
|
this.get('_timedSave').cancelAll();
|
|
},
|
|
|
|
save(options) {
|
|
return this.get('save').perform(options);
|
|
},
|
|
|
|
setSaveType(newType) {
|
|
if (newType === 'publish') {
|
|
this.set('willPublish', true);
|
|
this.set('willSchedule', false);
|
|
} else if (newType === 'draft') {
|
|
this.set('willPublish', false);
|
|
this.set('willSchedule', false);
|
|
} else if (newType === 'schedule') {
|
|
this.set('willSchedule', true);
|
|
this.set('willPublish', false);
|
|
}
|
|
},
|
|
|
|
closeNavMenu() {
|
|
this.get('application').send('closeAutoNav');
|
|
},
|
|
|
|
closeMenus() {
|
|
this.get('application').send('closeMenus');
|
|
},
|
|
|
|
toggleLeaveEditorModal(transition) {
|
|
let leaveTransition = this.get('leaveEditorTransition');
|
|
|
|
if (!transition && this.get('showLeaveEditorModal')) {
|
|
this.set('showLeaveEditorModal', false);
|
|
return;
|
|
}
|
|
|
|
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
|
this.set('leaveEditorTransition', transition);
|
|
|
|
// if a save is running, wait for it to finish then transition
|
|
if (this.get('saveTasks.isRunning')) {
|
|
return this.get('saveTasks.last').then(() => {
|
|
transition.retry();
|
|
});
|
|
}
|
|
|
|
// if an autosave is scheduled, cancel it, save then transition
|
|
if (this.get('_autosaveRunning')) {
|
|
this.send('cancelAutosave');
|
|
this.get('autosave').cancelAll();
|
|
|
|
return this.get('autosave').perform().then(() => {
|
|
transition.retry();
|
|
});
|
|
}
|
|
|
|
// we genuinely have unsaved data, show the modal
|
|
this.set('showLeaveEditorModal', true);
|
|
}
|
|
},
|
|
|
|
leaveEditor() {
|
|
let transition = this.get('leaveEditorTransition');
|
|
let model = this.get('model');
|
|
|
|
if (!transition) {
|
|
this.get('notifications').showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
|
return;
|
|
}
|
|
|
|
// definitely want to clear the data store and post of any unsaved, client-generated tags
|
|
model.updateTags();
|
|
|
|
if (model.get('isNew')) {
|
|
// the user doesn't want to save the new, unsaved post, so delete it.
|
|
model.deleteRecord();
|
|
} else {
|
|
// roll back changes on model props
|
|
model.rollbackAttributes();
|
|
}
|
|
|
|
// setting hasDirtyAttributes to false here allows willTransition on the editor route to succeed
|
|
this.set('hasDirtyAttributes', false);
|
|
|
|
// since the transition is now certain to complete, we can unset window.onbeforeunload here
|
|
window.onbeforeunload = null;
|
|
|
|
return transition.retry();
|
|
},
|
|
|
|
updateTitle(newTitle) {
|
|
this.set('model.titleScratch', newTitle);
|
|
},
|
|
|
|
toggleDeletePostModal() {
|
|
if (!this.get('model.isNew')) {
|
|
this.toggleProperty('showDeletePostModal');
|
|
}
|
|
},
|
|
|
|
toggleReAuthenticateModal() {
|
|
this.toggleProperty('showReAuthenticateModal');
|
|
},
|
|
|
|
setWordcount(wordcount) {
|
|
this.set('wordcount', wordcount);
|
|
},
|
|
|
|
toggleAutoNav() {
|
|
this.get('application').send('toggleAutoNav');
|
|
}
|
|
}
|
|
});
|