Ghost-Admin/app/services/custom-views.js

243 lines
6.5 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')
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) {
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();
}
}