Ghost/ghost/admin/app/services/custom-views.js
Kevin Ansfield 060d791a63 Removed need for .get() with settings service
no issue

The `settings` service has been a source of confusion when writing with modern Ember patterns because it's use of the deprecated `ProxyMixin` forced all property access/setting to go via `.get()` and `.set()` whereas the rest of the system has mostly (there are a few other uses of ProxyObjects remaining) eliminated the use of the non-native get/set methods.

- removed use of `ProxyMixin` in the `settings` service by grabbing the attributes off the setting model after fetching and using `Object.defineProperty()` to add native getters/setters that pass through to the model's getters/setters. Ember's autotracking automatically works across the native getters/setters so we can then use the service as if it was any other native object
- updated all code to use `settings.{attrName}` directly for getting/setting instead of `.get()` and `.set()`
- removed use of observer in the `customViews` service because it was being set up before the native properties had been added on the settings service meaning autotracking wasn't able to set up properly
2022-10-07 16:14:57 +01:00

234 lines
6.3 KiB
JavaScript

import CustomViewFormModal from '../components/modals/custom-view-form';
import EmberObject, {action} from '@ember/object';
import Service, {inject as service} from '@ember/service';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {isArray} from '@ember/array';
import {task} from 'ember-concurrency';
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: 'pen',
filter: {
type: 'draft'
}
}, {
route: 'posts',
name: 'Scheduled',
color: 'midgrey',
icon: 'clock',
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 modals;
@service router;
@service session;
@service settings;
get viewList() {
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.sharedViews || '[]');
views = isArray(views) ? views : [];
const 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
if (!session.user.isContributor) {
viewList.push(...DEFAULT_VIEWS);
}
viewList.push(...views.map((view) => {
return CustomView.create(view);
}));
return viewList;
}
@task
*saveViewTask(view) {
yield view.validate();
const {viewList} = this;
// perform some ad-hoc validation of duplicate names because ValidationEngine doesn't support it
let duplicateView = 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 = viewList.find(existingView => isViewEqual(existingView, view));
if (matchingView) {
viewList.replace(viewList.indexOf(matchingView), 1, [view]);
} else {
viewList.push(view);
}
// rebuild the "views" array in our user settings json string
yield this._saveViewSettings(viewList);
view.set('isNew', false);
return view;
}
@task
*deleteViewTask(view) {
const {viewList} = this;
let matchingView = viewList.find(existingView => isViewEqual(existingView, view));
if (matchingView && !matchingView.isDefault) {
viewList.removeObject(matchingView);
yield this._saveViewSettings(viewList);
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
});
}
@action
editView() {
const customView = CustomView.create(this.activeView || this.newView());
return this.modals.open(CustomViewFormModal, {
customView
});
}
async _saveViewSettings(viewList) {
let sharedViews = viewList.reject(view => view.isDefault).map(view => view.toJSON());
this.settings.sharedViews = JSON.stringify(sharedViews);
return this.settings.save();
}
}