Files
outline/app/stores/BaseStore.js
Tom Moor b42e9737b6 feat: Memberships (#1032)
* WIP

* feat: Add collection.memberships endpoint

* feat: Add ability to filter collection.memberships with query

* WIP

* Merge stashed work

* feat: Add ability to filter memberships by permission

* continued refactoring

* paginated list component

* Collection member management

* fix: Incorrect policy data sent down after collection.update

* Reduce duplication, add empty state

* cleanup

* fix: Modal close should be a real button

* fix: Allow opening edit from modal

* fix: remove unused methods

* test: fix

* Passing test suite

* Refactor

* fix: Flow UI errors

* test: Add collections.update tests

* lint

* test: moar tests

* fix: Missing scopes, more missing tests

* fix: Handle collection privacy change over socket

* fix: More membership scopes

* fix: view endpoint permissions

* fix: respond to privacy change on socket event

* policy driven menus

* fix: share endpoint policies

* chore: Use policies to drive documents UI

* alignment

* fix: Header height

* fix: Correct behavior when collection becomes private

* fix: Header height for read-only collection

* send id's over socket instead of serialized objects

* fix: Remote policy change

* fix: reduce collection fetching

* More websocket efficiencies

* fix: Document collection pinning

* fix: Restored ability to edit drafts
fix: Removed ability to star drafts

* fix: Require write permissions to pin doc to collection

* fix: Header title overlaying document actions at small screen sizes

* fix: Jank on load caused by previous commit

* fix: Double collection fetch post-publish

* fix: Hide publish button if draft is in no longer accessible collection

* fix: Always allow deleting drafts
fix: Improved handling of deleted documents

* feat: Show collections in drafts view
feat: Show more obvious 'draft' badge on documents

* fix: incorrect policies after publish to private collection

* fix: Duplicating a draft publishes it
2019-10-05 18:42:03 -07:00

186 lines
4.4 KiB
JavaScript

// @flow
import invariant from 'invariant';
import { observable, set, action, computed, runInAction } from 'mobx';
import { orderBy } from 'lodash';
import { client } from 'utils/ApiClient';
import RootStore from 'stores/RootStore';
import BaseModel from '../models/BaseModel';
import type { PaginationParams } from 'types';
type Action = 'list' | 'info' | 'create' | 'update' | 'delete';
function modelNameFromClassName(string) {
return string.charAt(0).toLowerCase() + string.slice(1);
}
export const DEFAULT_PAGINATION_LIMIT = 25;
export default class BaseStore<T: BaseModel> {
@observable data: Map<string, T> = new Map();
@observable isFetching: boolean = false;
@observable isSaving: boolean = false;
@observable isLoaded: boolean = false;
model: Class<T>;
modelName: string;
rootStore: RootStore;
actions: Action[] = ['list', 'info', 'create', 'update', 'delete'];
constructor(rootStore: RootStore, model: Class<T>) {
this.rootStore = rootStore;
this.model = model;
this.modelName = modelNameFromClassName(model.name);
}
@action
clear() {
this.data.clear();
}
addPolicies = policies => {
if (policies) {
policies.forEach(policy => this.rootStore.policies.add(policy));
}
};
@action
add = (item: Object): T => {
const Model = this.model;
if (!(item instanceof Model)) {
const existing: ?T = this.data.get(item.id);
if (existing) {
set(existing, item);
return existing;
} else {
item = new Model(item, this);
}
}
this.data.set(item.id, item);
return item;
};
@action
remove(id: string): void {
this.data.delete(id);
}
save(params: Object) {
if (params.id) return this.update(params);
return this.create(params);
}
get(id: string): ?T {
return this.data.get(id);
}
@action
async create(params: Object) {
if (!this.actions.includes('create')) {
throw new Error(`Cannot create ${this.modelName}`);
}
this.isSaving = true;
try {
const res = await client.post(`/${this.modelName}s.create`, params);
invariant(res && res.data, 'Data should be available');
this.addPolicies(res.policies);
return this.add(res.data);
} finally {
this.isSaving = false;
}
}
@action
async update(params: Object): * {
if (!this.actions.includes('update')) {
throw new Error(`Cannot update ${this.modelName}`);
}
this.isSaving = true;
try {
const res = await client.post(`/${this.modelName}s.update`, params);
invariant(res && res.data, 'Data should be available');
this.addPolicies(res.policies);
return this.add(res.data);
} finally {
this.isSaving = false;
}
}
@action
async delete(item: T) {
if (!this.actions.includes('delete')) {
throw new Error(`Cannot delete ${this.modelName}`);
}
this.isSaving = true;
try {
await client.post(`/${this.modelName}s.delete`, { id: item.id });
return this.remove(item.id);
} finally {
this.isSaving = false;
}
}
@action
async fetch(id: string, options?: Object = {}): Promise<*> {
if (!this.actions.includes('info')) {
throw new Error(`Cannot fetch ${this.modelName}`);
}
let item = this.data.get(id);
if (item && !options.force) return item;
this.isFetching = true;
try {
const res = await client.post(`/${this.modelName}s.info`, { id });
invariant(res && res.data, 'Data should be available');
this.addPolicies(res.policies);
return this.add(res.data);
} catch (err) {
if (err.statusCode === 403) {
this.remove(id);
}
throw err;
} finally {
this.isFetching = false;
}
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
if (!this.actions.includes('list')) {
throw new Error(`Cannot list ${this.modelName}`);
}
this.isFetching = true;
try {
const res = await client.post(`/${this.modelName}s.list`, params);
invariant(res && res.data, 'Data not available');
runInAction(`list#${this.modelName}`, () => {
this.addPolicies(res.policies);
res.data.forEach(this.add);
this.isLoaded = true;
});
return res.data;
} finally {
this.isFetching = false;
}
};
@computed
get orderedData(): T[] {
return orderBy(Array.from(this.data.values()), 'createdAt', 'desc');
}
}