mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Duplicated editor screens to react-editor
no issue - initial set up ready for testing use of react components (specifically an editor component for this experiment) inside of Admin - added `react-editor` route - duplicated all editor screen files and updated route references where necessary
This commit is contained in:
parent
40240d6b7a
commit
a77388159c
9 changed files with 1433 additions and 1 deletions
1094
ghost/admin/app/controllers/react-editor.js
vendored
Normal file
1094
ghost/admin/app/controllers/react-editor.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
9
ghost/admin/app/controllers/react-editor/edit-loading.js
vendored
Normal file
9
ghost/admin/app/controllers/react-editor/edit-loading.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Controller from '@ember/controller';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
@classic
|
||||
export default class ReactEditLoadingController extends Controller {
|
||||
@service ui;
|
||||
}
|
|
@ -35,6 +35,11 @@ Router.map(function () {
|
|||
this.route('edit', {path: ':type/:post_id'});
|
||||
});
|
||||
|
||||
this.route('react-editor', function () {
|
||||
this.route('new', {path: ':type'});
|
||||
this.route('edit', {path: ':type/:post_id'});
|
||||
});
|
||||
|
||||
this.route('tags');
|
||||
this.route('tag.new', {path: '/tags/new'});
|
||||
this.route('tag', {path: '/tags/:tag_slug'});
|
||||
|
@ -43,7 +48,7 @@ Router.map(function () {
|
|||
this.route('settings.general', {path: '/settings/general'});
|
||||
this.route('settings.membership', {path: '/settings/members'});
|
||||
this.route('settings.code-injection', {path: '/settings/code-injection'});
|
||||
|
||||
|
||||
// redirect from old /settings/members-email to /settings/newsletters
|
||||
this.route('settings.members-email', {path: '/settings/members-email'});
|
||||
this.route('settings.newsletters', {path: '/settings/newsletters'}, function () {
|
||||
|
|
66
ghost/admin/app/routes/react-editor.js
vendored
Normal file
66
ghost/admin/app/routes/react-editor.js
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
import $ from 'jquery';
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend({
|
||||
feature: service(),
|
||||
notifications: service(),
|
||||
ui: service(),
|
||||
|
||||
classNames: ['editor'],
|
||||
|
||||
activate() {
|
||||
this._super(...arguments);
|
||||
this.ui.set('isFullScreen', true);
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.ui.set('isFullScreen', false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this._blurAndScheduleAction(function () {
|
||||
this.controller.send('save');
|
||||
});
|
||||
},
|
||||
|
||||
authorizationFailed() {
|
||||
this.controller.send('toggleReAuthenticateModal');
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
// exit early if an upgrade is required because our extended route
|
||||
// class will abort the transition and show an error
|
||||
if (this.get('upgradeStatus.isRequired')) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
|
||||
this.controller.willTransition(transition);
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: () => {
|
||||
return this.get('controller.post.title') || 'Editor';
|
||||
},
|
||||
bodyClasses: ['gh-body-fullscreen'],
|
||||
mainClasses: ['gh-main-white']
|
||||
};
|
||||
},
|
||||
|
||||
_blurAndScheduleAction(func) {
|
||||
let selectedElement = $(document.activeElement);
|
||||
|
||||
// TODO: we should trigger a blur for textareas as well as text inputs
|
||||
if (selectedElement.is('input[type="text"]')) {
|
||||
selectedElement.trigger('focusout');
|
||||
}
|
||||
|
||||
// wait for actions triggered by the focusout to finish before saving
|
||||
run.scheduleOnce('actions', this, func);
|
||||
}
|
||||
});
|
67
ghost/admin/app/routes/react-editor/edit.js
vendored
Normal file
67
ghost/admin/app/routes/react-editor/edit.js
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import {pluralize} from 'ember-inflector';
|
||||
|
||||
export default class EditRoute extends AuthenticatedRoute {
|
||||
beforeModel(transition) {
|
||||
super.beforeModel(...arguments);
|
||||
|
||||
// if the transition is not new->edit, reset the post on the controller
|
||||
// so that the editor view is cleared before showing the loading state
|
||||
if (transition.urlMethod !== 'replace') {
|
||||
let editor = this.controllerFor('react-editor');
|
||||
editor.set('post', null);
|
||||
editor.reset();
|
||||
}
|
||||
}
|
||||
|
||||
model(params, transition) {
|
||||
// eslint-disable-next-line camelcase
|
||||
let {type: modelName, post_id} = params;
|
||||
|
||||
if (!['post', 'page'].includes(modelName)) {
|
||||
let path = transition.intent.url.replace(/^\//, '');
|
||||
return this.replaceWith('error404', {path, status: 404});
|
||||
}
|
||||
|
||||
let query = {
|
||||
// eslint-disable-next-line camelcase
|
||||
id: post_id
|
||||
};
|
||||
|
||||
return this.store.query(modelName, query)
|
||||
.then(records => records.get('firstObject'));
|
||||
}
|
||||
|
||||
// the API will return a post even if the logged in user doesn't have
|
||||
// permission to edit it (all posts are public) so we need to do our
|
||||
// own permissions check and redirect if necessary
|
||||
afterModel(post) {
|
||||
super.afterModel(...arguments);
|
||||
|
||||
const user = this.session.user;
|
||||
const returnRoute = pluralize(post.constructor.modelName);
|
||||
|
||||
if (user.isAuthorOrContributor && !post.isAuthoredByUser(user)) {
|
||||
return this.replaceWith(returnRoute);
|
||||
}
|
||||
|
||||
// If the post is not a draft and user is contributor, redirect to index
|
||||
if (user.isContributor && !post.isDraft) {
|
||||
return this.replaceWith(returnRoute);
|
||||
}
|
||||
}
|
||||
|
||||
serialize(model) {
|
||||
return {
|
||||
type: model.constructor.modelName,
|
||||
post_id: model.id
|
||||
};
|
||||
}
|
||||
|
||||
// there's no specific controller for this route, instead all editor
|
||||
// handling is done on the editor route/controller
|
||||
setupController(controller, post) {
|
||||
let editor = this.controllerFor('react-editor');
|
||||
editor.setPost(post);
|
||||
}
|
||||
}
|
8
ghost/admin/app/routes/react-editor/index.js
vendored
Normal file
8
ghost/admin/app/routes/react-editor/index.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default class IndexRoute extends AuthenticatedRoute {
|
||||
beforeModel() {
|
||||
super.beforeModel(...arguments);
|
||||
this.replaceWith('react-editor.new', 'post');
|
||||
}
|
||||
}
|
27
ghost/admin/app/routes/react-editor/new.js
vendored
Normal file
27
ghost/admin/app/routes/react-editor/new.js
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default class NewRoute extends AuthenticatedRoute {
|
||||
model(params, transition) {
|
||||
let {type: modelName} = params;
|
||||
|
||||
if (!['post','page'].includes(modelName)) {
|
||||
let path = transition.intent.url.replace(/^\//, '');
|
||||
return this.replaceWith('error404', {path, status: 404});
|
||||
}
|
||||
|
||||
return this.store.createRecord(modelName, {authors: [this.session.user]});
|
||||
}
|
||||
|
||||
// there's no specific controller for this route, instead all editor
|
||||
// handling is done on the editor route/controler
|
||||
setupController(controller, newPost) {
|
||||
let editor = this.controllerFor('react-editor');
|
||||
editor.setPost(newPost);
|
||||
}
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
mainClasses: ['editor-new']
|
||||
};
|
||||
}
|
||||
}
|
151
ghost/admin/app/templates/react-editor.hbs
Normal file
151
ghost/admin/app/templates/react-editor.hbs
Normal file
|
@ -0,0 +1,151 @@
|
|||
{{#if this.post}}
|
||||
<div class="flex flex-row">
|
||||
<GhEditor
|
||||
@tagName="section"
|
||||
@class="gh-editor gh-view relative"
|
||||
as |editor|
|
||||
>
|
||||
<header class="gh-editor-header br2 pe-none">
|
||||
<Editor::PublishManagement
|
||||
@post={{this.post}}
|
||||
@hasUnsavedChanges={{this.hasDirtyAttributes}}
|
||||
@beforePublish={{perform this.beforeSaveTask}}
|
||||
@afterPublish={{this.afterSave}}
|
||||
@saveTask={{this.saveTask}}
|
||||
as |publishManagement|
|
||||
>
|
||||
<div class="flex items-center pe-auto h-100">
|
||||
{{#if this.ui.isFullScreen}}
|
||||
<LinkTo @route={{pluralize this.post.displayName }} class="gh-btn-editor gh-editor-back-button" data-test-link={{pluralize this.post.displayName}}>
|
||||
<span>
|
||||
{{svg-jar "arrow-left"}}
|
||||
{{capitalize (pluralize this.post.displayName)}}
|
||||
</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
<div class="gh-editor-post-status">
|
||||
<span>
|
||||
<GhEditorPostStatus
|
||||
@post={{this.post}}
|
||||
@hasDirtyAttributes={{this.hasDirtyAttributes}}
|
||||
@isSaving={{or this.autosaveTask.isRunning this.saveTasks.isRunning}}
|
||||
@openUpdateFlow={{publishManagement.openUpdateFlow}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="flex items-center pe-auto h-100">
|
||||
{{#unless this.post.isNew}}
|
||||
<Editor::PublishButtons @publishManagement={{publishManagement}} />
|
||||
|
||||
{{#unless this.showSettingsMenu}}
|
||||
<div class="settings-menu-toggle-spacer"></div>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</section>
|
||||
</Editor::PublishManagement>
|
||||
</header>
|
||||
|
||||
{{!--
|
||||
gh-koenig-editor acts as a wrapper around the title input and
|
||||
koenig editor canvas to support Ghost-specific editor behaviour
|
||||
--}}
|
||||
<GhKoenigEditor
|
||||
@title={{readonly this.post.titleScratch}}
|
||||
@titleAutofocus={{this.shouldFocusTitle}}
|
||||
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
|
||||
@onTitleChange={{this.updateTitleScratch}}
|
||||
@onTitleBlur={{perform this.saveTitleTask}}
|
||||
@body={{readonly this.post.scratch}}
|
||||
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}
|
||||
@onBodyChange={{this.updateScratch}}
|
||||
@headerOffset={{editor.headerHeight}}
|
||||
@scrollContainerSelector=".gh-koenig-editor"
|
||||
@scrollOffsetBottomSelector=".gh-mobile-nav-bar"
|
||||
@onEditorCreated={{this.setKoenigEditor}}
|
||||
@onWordCountChange={{this.updateWordCount}}
|
||||
@snippets={{this.snippets}}
|
||||
@saveSnippet={{if this.canManageSnippets this.saveSnippet}}
|
||||
@updateSnippet={{if this.canManageSnippets this.toggleUpdateSnippetModal}}
|
||||
@deleteSnippet={{if this.canManageSnippets this.toggleDeleteSnippetModal}}
|
||||
@featureImage={{this.post.featureImage}}
|
||||
@featureImageAlt={{this.post.featureImageAlt}}
|
||||
@featureImageCaption={{this.post.featureImageCaption}}
|
||||
@setFeatureImage={{this.setFeatureImage}}
|
||||
@setFeatureImageAlt={{this.setFeatureImageAlt}}
|
||||
@setFeatureImageCaption={{this.setFeatureImageCaption}}
|
||||
@clearFeatureImage={{this.clearFeatureImage}}
|
||||
@cardOptions={{hash
|
||||
post=this.post
|
||||
}}
|
||||
@postType={{this.post.displayName}}
|
||||
/>
|
||||
|
||||
<div class="gh-editor-wordcount-container">
|
||||
<div class="gh-editor-wordcount">
|
||||
{{gh-pluralize this.wordCount.wordCount "word"}}
|
||||
</div>
|
||||
<a href="https://ghost.org/help/using-the-editor/" class="flex" target="_blank" rel="noopener noreferrer">{{svg-jar "help"}}</a>
|
||||
</div>
|
||||
|
||||
</GhEditor>
|
||||
|
||||
{{#if this.showSettingsMenu}}
|
||||
<GhPostSettingsMenu
|
||||
@post={{this.post}}
|
||||
@deletePost={{this.openDeletePostModal}}
|
||||
@updateSlugTask={{this.updateSlugTask}}
|
||||
@savePostTask={{this.savePostTask}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<button type="button" class="settings-menu-toggle gh-btn gh-btn-editor gh-btn-icon icon-only gh-btn-action-icon" title="Settings" {{on "click" this.toggleSettingsMenu}} data-test-psm-trigger>
|
||||
{{#if this.showSettingsMenu}}
|
||||
<span class="settings-menu-open">{{svg-jar "sidemenu-open"}}</span>
|
||||
{{else}}
|
||||
<span>{{svg-jar "sidemenu"}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
|
||||
{{#if this.showReAuthenticateModal}}
|
||||
<GhFullscreenModal @modal="re-authenticate"
|
||||
@close={{this.toggleReAuthenticateModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showUpgradeModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="upgrade-host-limit"
|
||||
@model={{hash
|
||||
message=this.hostLimitError.context
|
||||
details=this.hostLimitError.details
|
||||
}}
|
||||
@close={{this.closeUpgradeModal}}
|
||||
@modifier="action wide"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.snippetToUpdate}}
|
||||
<GhFullscreenModal
|
||||
@modal="update-snippet"
|
||||
@model={{this.snippetToUpdate}}
|
||||
@confirm={{this.updateSnippet}}
|
||||
@close={{this.toggleUpdateSnippetModal}}
|
||||
@modifier="action wide"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.snippetToDelete}}
|
||||
<GhFullscreenModal
|
||||
@modal="delete-snippet"
|
||||
@model={{this.snippetToDelete}}
|
||||
@confirm={{this.deleteSnippet}}
|
||||
@close={{this.toggleDeleteSnippetModal}}
|
||||
@modifier="action wide"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
5
ghost/admin/app/templates/react-editor/edit-loading.hbs
Normal file
5
ghost/admin/app/templates/react-editor/edit-loading.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="gh-view" {{did-insert (action "setMainClass" "gh-main-white" target=this.ui)}}>
|
||||
<div class="gh-content">
|
||||
<GhLoadingSpinner />
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Reference in a new issue