mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 04:43:12 +03:00
c646e78fff
no issue Having `session.user` return a promise made dealing with it in components difficult because you always had to remember it returned a promise rather than a model and had to handle the async behaviour. It also meant that you couldn't use any current user properties directly inside getters which made refactors to Glimmer/Octane idioms harder to reason about. `session.user` was a cached computed property so it really made no sense for it to be a promise - it was loaded on first access and then always returned instantly but with a fulfilled promise rather than the underlying model. Refactoring to a synchronous property that is loaded as part of the authentication flows (we load the current user to check that we're logged in - we may as well make use of that!) means one less thing to be aware of/remember and provides a nicer migration process to Glimmer components. As part of the refactor, the auth flows and pre-load of required data across other services was also simplified to make it easier to find and follow. - refactored app setup and `session.user` - added `session.populateUser()` that fetches a user model from the current user endpoint and sets it on `session.user` - removed knowledge of app setup from the `cookie` authenticator and moved it into = `session.postAuthPreparation()`, this means we have the same post-authentication setup no matter which authenticator is used so we have more consistent behaviour in tests which don't use the `cookie` authenticator - switched `session` service to native class syntax to get the expected `super()` behaviour - updated `handleAuthentication()` so it populate's `session.user` and performs post-auth setup before transitioning (handles sign-in after app load) - updated `application` route to remove duplicated knowledge of app preload behaviour that now lives in `session.postAuthPreparation()` (handles already-authed app load) - removed out-of-date attempt at pre-loading data from setup controller as that's now handled automatically via `session.handleAuthentication` - updated app code to not treat `session.user` as a promise - predominant usage was router `beforeModel` hooks that transitioned users without valid permissions, this sets us up for an easier removal of the `current-user-settings` mixin in the future
243 lines
6.6 KiB
JavaScript
243 lines
6.6 KiB
JavaScript
import EmberObject, {action} from '@ember/object';
|
|
import Service from '@ember/service';
|
|
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
|
import {isArray} from '@ember/array';
|
|
import {observes} from '@ember-decorators/object';
|
|
import {inject as service} from '@ember/service';
|
|
import {task} from 'ember-concurrency-decorators';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
const VIEW_COLORS = [
|
|
'midgrey',
|
|
'blue',
|
|
'green',
|
|
'red',
|
|
'teal',
|
|
'purple',
|
|
'yellow',
|
|
'orange',
|
|
'pink'
|
|
];
|
|
|
|
const CustomView = EmberObject.extend(ValidationEngine, {
|
|
validationType: 'customView',
|
|
|
|
name: '',
|
|
route: '',
|
|
color: '',
|
|
filter: null,
|
|
isNew: false,
|
|
isDefault: false,
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
if (!this.filter) {
|
|
this.filter = {};
|
|
}
|
|
if (!this.color) {
|
|
this.color = VIEW_COLORS[Math.floor(Math.random() * VIEW_COLORS.length)];
|
|
}
|
|
},
|
|
|
|
// convert to POJO so we don't store any client-specific objects in any
|
|
// stringified JSON settings fields
|
|
toJSON() {
|
|
return {
|
|
name: this.name,
|
|
route: this.route,
|
|
color: this.color,
|
|
filter: this.filter
|
|
};
|
|
}
|
|
});
|
|
|
|
const DEFAULT_VIEWS = [{
|
|
route: 'posts',
|
|
name: 'Drafts',
|
|
color: 'midgrey',
|
|
icon: 'pencil',
|
|
filter: {
|
|
type: 'draft'
|
|
}
|
|
}, {
|
|
route: 'posts',
|
|
name: 'Scheduled',
|
|
color: 'midgrey',
|
|
icon: 'clockface',
|
|
filter: {
|
|
type: 'scheduled'
|
|
}
|
|
}, {
|
|
route: 'posts',
|
|
name: 'Published',
|
|
color: 'midgray',
|
|
icon: 'published-post',
|
|
filter: {
|
|
type: 'published'
|
|
}
|
|
}].map((view) => {
|
|
return CustomView.create(Object.assign({}, view, {isDefault: true}));
|
|
});
|
|
|
|
let isFilterEqual = function (filterA, filterB) {
|
|
let aProps = Object.getOwnPropertyNames(filterA);
|
|
let bProps = Object.getOwnPropertyNames(filterB);
|
|
|
|
if (aProps.length !== bProps.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < aProps.length; i++) {
|
|
let key = aProps[i];
|
|
if (filterA[key] !== filterB[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
let isViewEqual = function (viewA, viewB) {
|
|
return viewA.route === viewB.route
|
|
&& isFilterEqual(viewA.filter, viewB.filter);
|
|
};
|
|
|
|
export default class CustomViewsService extends Service {
|
|
@service router;
|
|
@service session;
|
|
@service settings;
|
|
|
|
@tracked viewList = [];
|
|
@tracked showFormModal = false;
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
this.updateViewList();
|
|
}
|
|
|
|
// eslint-disable-next-line ghost/ember/no-observers
|
|
@observes('settings.sharedViews', 'session.{isAuthenticated,user}')
|
|
async updateViewList() {
|
|
let {settings, session} = this;
|
|
|
|
// avoid fetching user before authenticated otherwise the 403 can fire
|
|
// during authentication and cause errors during setup/signin
|
|
if (!session.isAuthenticated || !session.user) {
|
|
return;
|
|
}
|
|
|
|
let views = JSON.parse(settings.get('sharedViews') || '[]');
|
|
views = isArray(views) ? views : [];
|
|
|
|
let viewList = [];
|
|
|
|
// contributors can only see their own draft posts so it doesn't make
|
|
// sense to show them default views which change the status/type filter
|
|
let user = await session.user;
|
|
if (!user.isContributor) {
|
|
viewList.push(...DEFAULT_VIEWS);
|
|
}
|
|
|
|
viewList.push(...views.map((view) => {
|
|
return CustomView.create(view);
|
|
}));
|
|
|
|
this.viewList = viewList;
|
|
}
|
|
|
|
@action
|
|
toggleFormModal() {
|
|
this.showFormModal = !this.showFormModal;
|
|
}
|
|
|
|
@task
|
|
*saveViewTask(view) {
|
|
yield view.validate();
|
|
|
|
// perform some ad-hoc validation of duplicate names because ValidationEngine doesn't support it
|
|
let duplicateView = this.viewList.find((existingView) => {
|
|
return existingView.route === view.route
|
|
&& existingView.name.trim().toLowerCase() === view.name.trim().toLowerCase()
|
|
&& !isFilterEqual(existingView.filter, view.filter);
|
|
});
|
|
if (duplicateView) {
|
|
view.errors.add('name', 'Has already been used');
|
|
view.hasValidated.pushObject('name');
|
|
view.invalidate();
|
|
return false;
|
|
}
|
|
|
|
// remove an older version of the view from our views list
|
|
// - we don't allow editing the filter and route+filter combos are unique
|
|
// - we create a new instance of a view from an existing one when editing to act as a "scratch" view
|
|
let matchingView = this.viewList.find(existingView => isViewEqual(existingView, view));
|
|
if (matchingView) {
|
|
this.viewList.replace(this.viewList.indexOf(matchingView), 1, [view]);
|
|
} else {
|
|
this.viewList.push(view);
|
|
}
|
|
|
|
// rebuild the "views" array in our user settings json string
|
|
yield this._saveViewSettings();
|
|
|
|
view.set('isNew', false);
|
|
return view;
|
|
}
|
|
|
|
@task
|
|
*deleteViewTask(view) {
|
|
let matchingView = this.viewList.find(existingView => isViewEqual(existingView, view));
|
|
if (matchingView && !matchingView.isDefault) {
|
|
this.viewList.removeObject(matchingView);
|
|
yield this._saveViewSettings();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
get availableColors() {
|
|
return VIEW_COLORS;
|
|
}
|
|
|
|
get forPosts() {
|
|
return this.viewList.filter(view => view.route === 'posts');
|
|
}
|
|
|
|
get forPages() {
|
|
return this.viewList.filter(view => view.route === 'pages');
|
|
}
|
|
|
|
get activeView() {
|
|
if (!this.router.currentRoute) {
|
|
return undefined;
|
|
}
|
|
return this.findView(this.router.currentRouteName, this.router.currentRoute.queryParams);
|
|
}
|
|
|
|
findView(routeName, queryParams) {
|
|
let _routeName = routeName.replace(/_loading$/, '');
|
|
|
|
return this.viewList.find((view) => {
|
|
return view.route === _routeName
|
|
&& isFilterEqual(view.filter, queryParams);
|
|
});
|
|
}
|
|
|
|
newView() {
|
|
return CustomView.create({
|
|
isNew: true,
|
|
route: this.router.currentRouteName,
|
|
filter: this.router.currentRoute.queryParams
|
|
});
|
|
}
|
|
|
|
editView() {
|
|
return CustomView.create(this.activeView || this.newView());
|
|
}
|
|
|
|
async _saveViewSettings() {
|
|
let sharedViews = this.viewList.reject(view => view.isDefault).map(view => view.toJSON());
|
|
this.settings.set('sharedViews', JSON.stringify(sharedViews));
|
|
return this.settings.save();
|
|
}
|
|
}
|