This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/stores/DocumentsStore.js

515 lines
13 KiB
JavaScript
Raw Normal View History

2017-06-28 03:59:53 +00:00
// @flow
import { observable, action, computed, runInAction } from 'mobx';
import {
without,
map,
find,
orderBy,
filter,
compact,
omitBy,
uniq,
} from 'lodash';
2017-06-28 03:59:53 +00:00
import { client } from 'utils/ApiClient';
import naturalSort from 'shared/utils/naturalSort';
2017-06-28 03:59:53 +00:00
import invariant from 'invariant';
import BaseStore from 'stores/BaseStore';
import RootStore from 'stores/RootStore';
import Document from 'models/Document';
import Revision from 'models/Revision';
import type { FetchOptions, PaginationParams, SearchResult } from 'types';
2017-07-16 18:47:48 +00:00
export default class DocumentsStore extends BaseStore<Document> {
@observable recentlyViewedIds: string[] = [];
@observable searchCache: Map<string, SearchResult[]> = new Map();
@observable starredIds: Map<string, boolean> = new Map();
@observable backlinks: Map<string, string[]> = new Map();
2017-07-18 05:24:19 +00:00
constructor(rootStore: RootStore) {
super(rootStore, Document);
}
2017-06-28 03:59:53 +00:00
2017-11-10 22:14:30 +00:00
@computed
get all(): Document[] {
return filter(this.orderedData, d => !d.archivedAt && !d.deletedAt);
}
@computed
get recentlyViewed(): Document[] {
return orderBy(
compact(this.recentlyViewedIds.map(id => this.data.get(id))),
'updatedAt',
'desc'
);
2017-07-09 18:26:17 +00:00
}
2017-11-10 22:14:30 +00:00
@computed
get recentlyUpdated(): Document[] {
return orderBy(this.all, 'updatedAt', 'desc');
2017-07-09 18:26:17 +00:00
}
createdByUser(userId: string): Document[] {
return orderBy(
filter(this.all, d => d.createdBy.id === userId),
2018-08-10 06:14:51 +00:00
'updatedAt',
'desc'
);
}
inCollection(collectionId: string): Document[] {
return filter(this.all, document => document.collectionId === collectionId);
}
pinnedInCollection(collectionId: string): Document[] {
return filter(
this.recentlyUpdatedInCollection(collectionId),
document => document.pinned
);
}
2019-01-08 07:42:55 +00:00
publishedInCollection(collectionId: string): Document[] {
return filter(
this.all,
2019-01-08 07:42:55 +00:00
document =>
document.collectionId === collectionId && !!document.publishedAt
);
}
leastRecentlyUpdatedInCollection(collectionId: string): Document[] {
return orderBy(
this.publishedInCollection(collectionId),
'updatedAt',
'asc'
);
}
recentlyUpdatedInCollection(collectionId: string): Document[] {
return orderBy(
2019-01-08 07:42:55 +00:00
this.publishedInCollection(collectionId),
2017-11-20 05:32:18 +00:00
'updatedAt',
'desc'
);
}
recentlyPublishedInCollection(collectionId: string): Document[] {
return orderBy(
2019-01-08 07:42:55 +00:00
this.publishedInCollection(collectionId),
'publishedAt',
'desc'
);
}
2019-01-08 07:42:55 +00:00
alphabeticalInCollection(collectionId: string): Document[] {
return naturalSort(this.publishedInCollection(collectionId), 'title');
}
searchResults(query: string): SearchResult[] {
return this.searchCache.get(query) || [];
}
2017-11-10 22:14:30 +00:00
@computed
get starred(): Document[] {
return filter(this.all, d => d.isStarred);
}
@computed
get archived(): Document[] {
return filter(
orderBy(this.orderedData, 'archivedAt', 'desc'),
d => d.archivedAt && !d.deletedAt
);
2017-07-09 18:26:17 +00:00
}
@computed
get deleted(): Document[] {
return filter(
orderBy(this.orderedData, 'deletedAt', 'desc'),
d => d.deletedAt
);
}
@computed
get starredAlphabetical(): Document[] {
return naturalSort(this.starred, 'title');
}
@computed
get drafts(): Document[] {
return filter(
orderBy(this.all, 'updatedAt', 'desc'),
doc => !doc.publishedAt
);
}
2017-11-10 22:14:30 +00:00
@computed
get active(): ?Document {
return this.rootStore.ui.activeDocumentId
? this.data.get(this.rootStore.ui.activeDocumentId)
: undefined;
}
2017-11-10 22:14:30 +00:00
@action
fetchBacklinks = async (documentId: string): Promise<?(Document[])> => {
const res = await client.post(`/documents.list`, {
backlinkDocumentId: documentId,
});
invariant(res && res.data, 'Document list not available');
const { data } = res;
runInAction('DocumentsStore#fetchBacklinks', () => {
data.forEach(this.add);
this.backlinks.set(documentId, data.map(doc => doc.id));
});
};
getBacklinedDocuments(documentId: string): Document[] {
const documentIds = this.backlinks.get(documentId) || [];
return orderBy(
compact(documentIds.map(id => this.data.get(id))),
'updatedAt',
'desc'
);
}
2019-12-23 01:06:39 +00:00
@action
fetchChildDocuments = async (documentId: string): Promise<?(Document[])> => {
const res = await client.post(`/documents.list`, {
parentDocumentId: documentId,
});
invariant(res && res.data, 'Document list not available');
const { data } = res;
runInAction('DocumentsStore#fetchChildDocuments', () => {
data.forEach(this.add);
});
};
@action
fetchNamedPage = async (
2017-12-04 00:50:50 +00:00
request: string = 'list',
options: ?PaginationParams
2018-08-11 21:02:37 +00:00
): Promise<?(Document[])> => {
this.isFetching = true;
2017-06-28 03:59:53 +00:00
try {
2017-09-03 21:12:37 +00:00
const res = await client.post(`/documents.${request}`, options);
2017-06-28 03:59:53 +00:00
invariant(res && res.data, 'Document list not available');
runInAction('DocumentsStore#fetchNamedPage', () => {
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-06 01:42:03 +00:00
res.data.forEach(this.add);
this.addPolicies(res.policies);
2017-06-28 03:59:53 +00:00
this.isLoaded = true;
});
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-06 01:42:03 +00:00
return res.data;
} finally {
this.isFetching = false;
2017-06-28 03:59:53 +00:00
}
};
@action
fetchArchived = async (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('archived', options);
};
@action
fetchDeleted = async (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('deleted', options);
};
2017-11-10 22:14:30 +00:00
@action
fetchRecentlyUpdated = async (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('list', options);
2017-09-03 21:12:37 +00:00
};
2019-01-08 07:42:55 +00:00
@action
fetchAlphabetical = async (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('list', {
sort: 'title',
direction: 'ASC',
...options,
});
};
@action
fetchLeastRecentlyUpdated = async (
options: ?PaginationParams
): Promise<*> => {
return this.fetchNamedPage('list', {
sort: 'updatedAt',
direction: 'ASC',
...options,
});
};
@action
fetchRecentlyPublished = async (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('list', {
sort: 'publishedAt',
direction: 'DESC',
...options,
});
};
2017-11-10 22:14:30 +00:00
@action
2017-12-04 00:50:50 +00:00
fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
const data = await this.fetchNamedPage('viewed', options);
2017-06-28 04:52:47 +00:00
runInAction('DocumentsStore#fetchRecentlyViewed', () => {
2018-08-26 22:27:32 +00:00
// $FlowFixMe
2018-08-11 21:02:37 +00:00
this.recentlyViewedIds.replace(
uniq(this.recentlyViewedIds.concat(map(data, 'id')))
);
2017-06-28 04:52:47 +00:00
});
2017-09-03 21:12:37 +00:00
return data;
2017-06-28 04:20:09 +00:00
};
2017-11-10 22:14:30 +00:00
@action
fetchStarred = (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('starred', options);
};
@action
fetchDrafts = (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('drafts', options);
2017-06-28 05:15:29 +00:00
};
@action
fetchPinned = (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('pinned', options);
};
2018-08-10 06:14:51 +00:00
@action
fetchOwned = (options: ?PaginationParams): Promise<*> => {
return this.fetchNamedPage('list', options);
2018-08-10 06:14:51 +00:00
};
2017-11-10 22:14:30 +00:00
@action
2017-12-04 00:50:50 +00:00
search = async (
query: string,
options: PaginationParams = {}
): Promise<SearchResult[]> => {
// $FlowFixMe
const compactedOptions = omitBy(options, o => !o);
2017-12-04 00:50:50 +00:00
const res = await client.get('/documents.search', {
...compactedOptions,
2017-12-04 00:50:50 +00:00
query,
});
invariant(res && res.data, 'Search response should be available');
// add the documents and associated policies to the store
res.data.forEach(result => this.add(result.document));
this.addPolicies(res.policies);
// store a reference to the document model in the search cache instead
// of the original result from the API.
const results: SearchResult[] = compact(
res.data.map(result => {
const document = this.data.get(result.document.id);
if (!document) return null;
return {
ranking: result.ranking,
context: result.context,
document,
};
})
);
let existing = this.searchCache.get(query) || [];
// splice modifies any existing results, taking into account pagination
existing.splice(options.offset || 0, options.limit || 0, ...results);
this.searchCache.set(query, existing);
return data;
2017-09-13 06:30:18 +00:00
};
2017-11-10 22:14:30 +00:00
@action
prefetchDocument = (id: string) => {
if (!this.data.get(id)) {
return this.fetch(id, { prefetch: true });
2018-05-13 20:26:06 +00:00
}
2017-10-20 07:27:01 +00:00
};
2017-11-10 22:14:30 +00:00
@action
fetch = async (
id: string,
options?: FetchOptions = {}
): Promise<?Document> => {
2018-05-13 20:26:06 +00:00
if (!options.prefetch) this.isFetching = true;
2017-06-28 03:59:53 +00:00
try {
const doc: ?Document = this.data.get(id) || this.getByUrl(id);
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-06 01:42:03 +00:00
const policy = doc ? this.rootStore.policies.get(doc.id) : undefined;
if (doc && policy && !options.force) {
return doc;
}
2018-05-13 20:26:06 +00:00
const res = await client.post('/documents.info', {
id,
shareId: options.shareId,
});
2017-06-28 03:59:53 +00:00
invariant(res && res.data, 'Document not available');
this.addPolicies(res.policies);
this.add(res.data);
2017-06-28 03:59:53 +00:00
runInAction('DocumentsStore#fetch', () => {
this.isLoaded = true;
});
return this.data.get(res.data.id);
} finally {
this.isFetching = false;
2017-06-28 03:59:53 +00:00
}
};
@action
move = async (
document: Document,
collectionId: string,
parentDocumentId: ?string
) => {
const res = await client.post('/documents.move', {
id: document.id,
collectionId,
parentDocumentId,
});
invariant(res && res.data, 'Data not available');
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
};
@action
duplicate = async (document: Document): * => {
const res = await client.post('/documents.create', {
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-06 01:42:03 +00:00
publish: !!document.publishedAt,
parentDocumentId: document.parentDocumentId,
2019-07-27 17:33:38 +00:00
collectionId: document.collectionId,
title: `${document.title} (duplicate)`,
text: document.text,
});
invariant(res && res.data, 'Data should be available');
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
this.addPolicies(res.policies);
return this.add(res.data);
2017-06-28 03:59:53 +00:00
};
_add = this.add;
@action
add = (item: Object) => {
const document = this._add(item);
if (item.starred !== undefined) {
this.starredIds.set(document.id, item.starred);
}
return document;
};
@action
removeCollectionDocuments(collectionId: string) {
const documents = this.inCollection(collectionId);
const documentIds = documents.map(doc => doc.id);
documentIds.forEach(id => this.remove(id));
}
@action
async update(params: {
id: string,
title: string,
text: string,
lastRevision: number,
}) {
const document = await super.update(params);
// Because the collection object contains the url and title
// we need to ensure they are updated there as well.
const collection = this.getCollectionForDocument(document);
if (collection) collection.updateDocument(document);
return document;
}
@action
async delete(document: Document) {
await super.delete(document);
runInAction(() => {
this.recentlyViewedIds = without(this.recentlyViewedIds, document.id);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
}
2017-11-10 22:14:30 +00:00
@action
archive = async (document: Document) => {
const res = await client.post('/documents.archive', {
id: document.id,
});
runInAction('Document#archive', () => {
invariant(res && res.data, 'Data should be available');
document.updateFromJson(res.data);
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-06 01:42:03 +00:00
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
};
@action
restore = async (document: Document, revision?: Revision) => {
const res = await client.post('/documents.restore', {
id: document.id,
revisionId: revision ? revision.id : undefined,
});
runInAction('Document#restore', () => {
invariant(res && res.data, 'Data should be available');
document.updateFromJson(res.data);
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-06 01:42:03 +00:00
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
2017-06-28 03:59:53 +00:00
};
pin = (document: Document) => {
return client.post('/documents.pin', { id: document.id });
2017-06-28 03:59:53 +00:00
};
unpin = (document: Document) => {
return client.post('/documents.unpin', { id: document.id });
};
star = async (document: Document) => {
this.starredIds.set(document.id, true);
try {
return client.post('/documents.star', { id: document.id });
} catch (err) {
this.starredIds.set(document.id, false);
}
};
unstar = (document: Document) => {
this.starredIds.set(document.id, false);
try {
return client.post('/documents.unstar', { id: document.id });
} catch (err) {
this.starredIds.set(document.id, false);
}
};
2017-07-16 18:47:48 +00:00
2019-01-20 02:14:10 +00:00
getByUrl = (url: string = ''): ?Document => {
return find(this.orderedData, doc => url.endsWith(doc.urlId));
};
getCollectionForDocument(document: Document) {
return this.rootStore.collections.data.get(document.collectionId);
2017-06-28 03:59:53 +00:00
}
}