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('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('tags');
|
||||||
this.route('tag.new', {path: '/tags/new'});
|
this.route('tag.new', {path: '/tags/new'});
|
||||||
this.route('tag', {path: '/tags/:tag_slug'});
|
this.route('tag', {path: '/tags/:tag_slug'});
|
||||||
|
|
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