mirror of
https://github.com/outline/outline.git
synced 2025-12-30 15:30:12 -06:00
838 lines
21 KiB
TypeScript
838 lines
21 KiB
TypeScript
import invariant from "invariant";
|
|
import compact from "lodash/compact";
|
|
import filter from "lodash/filter";
|
|
import find from "lodash/find";
|
|
import omitBy from "lodash/omitBy";
|
|
import orderBy from "lodash/orderBy";
|
|
import { observable, action, computed, runInAction } from "mobx";
|
|
import type {
|
|
DateFilter,
|
|
NavigationNode,
|
|
PublicTeam,
|
|
StatusFilter,
|
|
} from "@shared/types";
|
|
import { subtractDate } from "@shared/utils/date";
|
|
import { bytesToHumanReadable } from "@shared/utils/files";
|
|
import naturalSort from "@shared/utils/naturalSort";
|
|
import RootStore from "~/stores/RootStore";
|
|
import Store from "~/stores/base/Store";
|
|
import Document from "~/models/Document";
|
|
import env from "~/env";
|
|
import type {
|
|
FetchOptions,
|
|
PaginationParams,
|
|
PartialExcept,
|
|
SearchResult,
|
|
} from "~/types";
|
|
import { client } from "~/utils/ApiClient";
|
|
import { extname } from "~/utils/files";
|
|
|
|
type FetchPageParams = PaginationParams & {
|
|
template?: boolean;
|
|
collectionId?: string;
|
|
};
|
|
|
|
export type SearchParams = {
|
|
offset?: number;
|
|
limit?: number;
|
|
dateFilter?: DateFilter;
|
|
statusFilter?: StatusFilter[];
|
|
collectionId?: string;
|
|
userId?: string;
|
|
shareId?: string;
|
|
};
|
|
|
|
type ImportOptions = {
|
|
publish?: boolean;
|
|
};
|
|
|
|
export default class DocumentsStore extends Store<Document> {
|
|
sharedCache: Map<
|
|
string,
|
|
{ sharedTree: NavigationNode; team: PublicTeam } | undefined
|
|
> = new Map();
|
|
|
|
@observable
|
|
backlinks: Map<string, string[]> = new Map();
|
|
|
|
@observable
|
|
movingDocumentId: string | null | undefined;
|
|
|
|
importFileTypes: string[] = [
|
|
".md",
|
|
".doc",
|
|
".docx",
|
|
"text/markdown",
|
|
"text/plain",
|
|
"text/html",
|
|
"application/msword",
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
];
|
|
|
|
constructor(rootStore: RootStore) {
|
|
super(rootStore, Document);
|
|
}
|
|
|
|
@computed
|
|
get all(): Document[] {
|
|
return filter(
|
|
this.orderedData,
|
|
(d) => !d.archivedAt && !d.deletedAt && !d.template
|
|
);
|
|
}
|
|
|
|
@computed
|
|
get recentlyViewed(): Document[] {
|
|
return orderBy(
|
|
this.all.filter((d) => d.lastViewedAt),
|
|
"lastViewedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
@computed
|
|
get recentlyUpdated(): Document[] {
|
|
return orderBy(this.all, "updatedAt", "desc");
|
|
}
|
|
|
|
get templates(): Document[] {
|
|
return orderBy(
|
|
filter(
|
|
this.orderedData,
|
|
(d) => !d.archivedAt && !d.deletedAt && d.template
|
|
),
|
|
"updatedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
createdByUser(userId: string): Document[] {
|
|
return orderBy(
|
|
filter(this.all, (d) => d.createdBy?.id === userId),
|
|
"updatedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
inCollection(collectionId: string): Document[] {
|
|
return filter(
|
|
this.all,
|
|
(document) => document.collectionId === collectionId
|
|
);
|
|
}
|
|
|
|
archivedInCollection(
|
|
collectionId: string,
|
|
options?: { archivedAt: string }
|
|
): Document[] {
|
|
const filterCond = (document: Document) =>
|
|
options
|
|
? document.collectionId === collectionId &&
|
|
document.isArchived &&
|
|
document.archivedAt === options.archivedAt &&
|
|
!document.isDeleted
|
|
: document.collectionId === collectionId &&
|
|
document.isArchived &&
|
|
!document.isDeleted;
|
|
|
|
return filter(this.orderedData, filterCond);
|
|
}
|
|
|
|
unarchivedInCollection(collectionId: string): Document[] {
|
|
return filter(
|
|
this.orderedData,
|
|
(document) =>
|
|
document.collectionId === collectionId &&
|
|
!document.isArchived &&
|
|
!document.isDeleted
|
|
);
|
|
}
|
|
|
|
templatesInCollection(collectionId: string): Document[] {
|
|
return orderBy(
|
|
filter(
|
|
this.orderedData,
|
|
(d) =>
|
|
!d.archivedAt &&
|
|
!d.deletedAt &&
|
|
d.template === true &&
|
|
d.collectionId === collectionId
|
|
),
|
|
"updatedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
publishedInCollection(collectionId: string): Document[] {
|
|
return filter(
|
|
this.all,
|
|
(document) =>
|
|
document.collectionId === collectionId && !!document.publishedAt
|
|
);
|
|
}
|
|
|
|
rootInCollection(collectionId: string): Document[] {
|
|
const collection = this.rootStore.collections.get(collectionId);
|
|
|
|
if (!collection || !collection.sortedDocuments) {
|
|
return [];
|
|
}
|
|
|
|
const drafts = this.drafts({ collectionId });
|
|
|
|
return compact([
|
|
...drafts,
|
|
...collection.sortedDocuments.map((node) => this.get(node.id)),
|
|
]);
|
|
}
|
|
|
|
leastRecentlyUpdatedInCollection(collectionId: string): Document[] {
|
|
return orderBy(this.inCollection(collectionId), "updatedAt", "asc");
|
|
}
|
|
|
|
recentlyUpdatedInCollection(collectionId: string): Document[] {
|
|
return orderBy(this.inCollection(collectionId), "updatedAt", "desc");
|
|
}
|
|
|
|
recentlyPublishedInCollection(collectionId: string): Document[] {
|
|
return orderBy(
|
|
this.publishedInCollection(collectionId),
|
|
"publishedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
alphabeticalInCollection(collectionId: string): Document[] {
|
|
return naturalSort(this.inCollection(collectionId), "title");
|
|
}
|
|
|
|
get(id: string): Document | undefined {
|
|
return (
|
|
this.data.get(id) ??
|
|
this.orderedData.find((doc) => id.endsWith(doc.urlId))
|
|
);
|
|
}
|
|
|
|
@computed
|
|
get archived(): Document[] {
|
|
return orderBy(this.orderedData, "archivedAt", "desc").filter(
|
|
(d) => d.archivedAt && !d.deletedAt
|
|
);
|
|
}
|
|
|
|
@computed
|
|
get deleted(): Document[] {
|
|
return orderBy(this.orderedData, "deletedAt", "desc").filter(
|
|
(d) => d.deletedAt
|
|
);
|
|
}
|
|
|
|
@computed
|
|
get templatesAlphabetical(): Document[] {
|
|
return naturalSort(this.templates, "title");
|
|
}
|
|
|
|
@computed
|
|
get totalDrafts(): number {
|
|
return this.drafts().length;
|
|
}
|
|
|
|
drafts = (
|
|
options: PaginationParams & {
|
|
dateFilter?: DateFilter;
|
|
collectionId?: string;
|
|
} = {}
|
|
): Document[] => {
|
|
let drafts = filter(
|
|
orderBy(this.all, "updatedAt", "desc"),
|
|
(doc) => !doc.publishedAt
|
|
);
|
|
|
|
if (options.dateFilter) {
|
|
drafts = filter(
|
|
drafts,
|
|
(draft) =>
|
|
new Date(draft.updatedAt) >=
|
|
subtractDate(new Date(), options.dateFilter || "year")
|
|
);
|
|
}
|
|
|
|
if (options.collectionId) {
|
|
drafts = filter(drafts, {
|
|
collectionId: options.collectionId,
|
|
});
|
|
}
|
|
|
|
return drafts;
|
|
};
|
|
|
|
@computed
|
|
get active(): Document | undefined {
|
|
return this.rootStore.ui.activeDocumentId
|
|
? this.data.get(this.rootStore.ui.activeDocumentId)
|
|
: undefined;
|
|
}
|
|
|
|
@action
|
|
fetchBacklinks = async (documentId: string): Promise<void> => {
|
|
const res = await client.post(`/documents.list`, {
|
|
backlinkDocumentId: documentId,
|
|
});
|
|
invariant(res?.data, "Document list not available");
|
|
const { data } = res;
|
|
|
|
runInAction("DocumentsStore#fetchBacklinks", () => {
|
|
data.forEach(this.add);
|
|
this.addPolicies(res.policies);
|
|
|
|
this.backlinks.set(
|
|
documentId,
|
|
data.map((doc: Partial<Document>) => doc.id)
|
|
);
|
|
});
|
|
};
|
|
|
|
getBacklinkedDocuments(documentId: string): Document[] {
|
|
const documentIds = this.backlinks.get(documentId) || [];
|
|
return orderBy(
|
|
compact(documentIds.map((id) => this.data.get(id))),
|
|
"updatedAt",
|
|
"desc"
|
|
);
|
|
}
|
|
|
|
getSharedTree(documentId: string): NavigationNode | undefined {
|
|
return this.sharedCache.get(documentId)?.sharedTree;
|
|
}
|
|
|
|
@action
|
|
fetchChildDocuments = async (documentId: string): Promise<void> => {
|
|
const res = await client.post(`/documents.list`, {
|
|
parentDocumentId: documentId,
|
|
});
|
|
invariant(res?.data, "Document list not available");
|
|
|
|
runInAction("DocumentsStore#fetchChildDocuments", () => {
|
|
res.data.forEach(this.add);
|
|
this.addPolicies(res.policies);
|
|
});
|
|
};
|
|
|
|
@action
|
|
fetchNamedPage = async (
|
|
request = "list",
|
|
options: FetchPageParams | undefined
|
|
): Promise<Document[]> => {
|
|
this.isFetching = true;
|
|
|
|
try {
|
|
const res = await client.post(`/documents.${request}`, options);
|
|
invariant(res?.data, "Document list not available");
|
|
runInAction("DocumentsStore#fetchNamedPage", () => {
|
|
res.data.forEach(this.add);
|
|
this.addPolicies(res.policies);
|
|
this.isLoaded = true;
|
|
});
|
|
return res.data;
|
|
} finally {
|
|
this.isFetching = false;
|
|
}
|
|
};
|
|
|
|
@action
|
|
fetchArchived = async (options?: PaginationParams): Promise<Document[]> => {
|
|
const archivedInResponse = await this.fetchNamedPage("archived", options);
|
|
const archivedInMemory = this.archived;
|
|
|
|
archivedInMemory.forEach((docInMemory) => {
|
|
!archivedInResponse.find(
|
|
(docInResponse) => docInResponse.id === docInMemory.id
|
|
) && this.remove(docInMemory.id);
|
|
});
|
|
|
|
return archivedInResponse;
|
|
};
|
|
|
|
@action
|
|
fetchDeleted = async (options?: PaginationParams): Promise<Document[]> =>
|
|
this.fetchNamedPage("deleted", options);
|
|
|
|
@action
|
|
fetchRecentlyUpdated = async (
|
|
options?: PaginationParams
|
|
): Promise<Document[]> => this.fetchNamedPage("list", options);
|
|
|
|
@action
|
|
fetchTemplates = async (options?: PaginationParams): Promise<Document[]> =>
|
|
this.fetchNamedPage("list", { ...options, template: true });
|
|
|
|
@action
|
|
fetchAlphabetical = async (options?: PaginationParams): Promise<Document[]> =>
|
|
this.fetchNamedPage("list", {
|
|
sort: "title",
|
|
direction: "ASC",
|
|
...options,
|
|
});
|
|
|
|
@action
|
|
fetchLeastRecentlyUpdated = async (
|
|
options?: PaginationParams
|
|
): Promise<Document[]> =>
|
|
this.fetchNamedPage("list", {
|
|
sort: "updatedAt",
|
|
direction: "ASC",
|
|
...options,
|
|
});
|
|
|
|
@action
|
|
fetchRecentlyPublished = async (
|
|
options?: PaginationParams
|
|
): Promise<Document[]> =>
|
|
this.fetchNamedPage("list", {
|
|
sort: "publishedAt",
|
|
direction: "DESC",
|
|
...options,
|
|
});
|
|
|
|
@action
|
|
fetchRecentlyViewed = async (
|
|
options?: PaginationParams
|
|
): Promise<Document[]> => this.fetchNamedPage("viewed", options);
|
|
|
|
@action
|
|
fetchStarred = (options?: PaginationParams): Promise<Document[]> =>
|
|
this.fetchNamedPage("starred", options);
|
|
|
|
@action
|
|
fetchDrafts = (options: PaginationParams = {}): Promise<Document[]> =>
|
|
this.fetchNamedPage("drafts", { limit: 100, ...options });
|
|
|
|
@action
|
|
fetchOwned = (options?: PaginationParams): Promise<Document[]> =>
|
|
this.fetchNamedPage("list", options);
|
|
|
|
@action
|
|
searchTitles = async (
|
|
query: string,
|
|
options?: SearchParams
|
|
): Promise<SearchResult[]> => {
|
|
const compactedOptions = omitBy(options, (o) => !o);
|
|
const res = await client.post("/documents.search_titles", {
|
|
...compactedOptions,
|
|
query,
|
|
});
|
|
invariant(res?.data, "Search response should be available");
|
|
|
|
// add the documents and associated policies to the store
|
|
runInAction("DocumentsStore#searchTitles", () => {
|
|
res.data.forEach(this.add);
|
|
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: SearchResult) => {
|
|
const document = this.data.get(result.id);
|
|
if (!document) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: document.id,
|
|
document,
|
|
};
|
|
})
|
|
);
|
|
return results;
|
|
};
|
|
|
|
@action
|
|
search = async (
|
|
query: string,
|
|
options: SearchParams
|
|
): Promise<SearchResult[]> => {
|
|
const compactedOptions = omitBy(options, (o) => !o);
|
|
const res = await client.post("/documents.search", {
|
|
...compactedOptions,
|
|
query,
|
|
});
|
|
invariant(res?.data, "Search response should be available");
|
|
|
|
// add the documents and associated policies to the store
|
|
runInAction("DocumentsStore#search", () => {
|
|
res.data.forEach((result: SearchResult) => 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: SearchResult) => {
|
|
const document = this.data.get(result.document.id);
|
|
if (!document) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: document.id,
|
|
ranking: result.ranking,
|
|
context: result.context,
|
|
document,
|
|
};
|
|
})
|
|
);
|
|
return results;
|
|
};
|
|
|
|
@action
|
|
prefetchDocument = async (id: string) => {
|
|
if (!this.data.get(id) && !this.getByUrl(id)) {
|
|
return this.fetch(id, {
|
|
prefetch: true,
|
|
});
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
@action
|
|
templatize = async ({
|
|
id,
|
|
collectionId,
|
|
publish,
|
|
}: {
|
|
id: string;
|
|
collectionId: string | null;
|
|
publish: boolean;
|
|
}): Promise<Document | null | undefined> => {
|
|
const doc: Document | null | undefined = this.data.get(id);
|
|
invariant(doc, "Document should exist");
|
|
|
|
if (doc.template) {
|
|
return;
|
|
}
|
|
|
|
const res = await client.post("/documents.templatize", {
|
|
id,
|
|
collectionId,
|
|
publish,
|
|
});
|
|
invariant(res?.data, "Document not available");
|
|
this.addPolicies(res.policies);
|
|
this.add(res.data);
|
|
return this.data.get(res.data.id);
|
|
};
|
|
|
|
override fetch = (id: string, options: FetchOptions = {}) =>
|
|
super.fetch(
|
|
id,
|
|
options,
|
|
(res: { data: { document: PartialExcept<Document, "id"> } }) =>
|
|
res.data.document
|
|
);
|
|
|
|
@action
|
|
fetchWithSharedTree = async (
|
|
id: string,
|
|
options: FetchOptions = {}
|
|
): Promise<{
|
|
document: Document;
|
|
team?: PublicTeam;
|
|
sharedTree?: NavigationNode;
|
|
}> => {
|
|
if (!options.prefetch) {
|
|
this.isFetching = true;
|
|
}
|
|
|
|
try {
|
|
const doc: Document | null | undefined =
|
|
this.data.get(id) || this.getByUrl(id);
|
|
const policy = doc ? this.rootStore.policies.get(doc.id) : undefined;
|
|
|
|
if (doc && policy && !options.shareId && !options.force) {
|
|
return {
|
|
document: doc,
|
|
};
|
|
}
|
|
|
|
if (
|
|
doc &&
|
|
options.shareId &&
|
|
!options.force &&
|
|
this.sharedCache.has(options.shareId)
|
|
) {
|
|
return {
|
|
document: doc,
|
|
...this.sharedCache.get(options.shareId),
|
|
};
|
|
}
|
|
|
|
const res = await client.post("/documents.info", {
|
|
id,
|
|
shareId: options.shareId,
|
|
});
|
|
|
|
invariant(res?.data, "Document not available");
|
|
this.addPolicies(res.policies);
|
|
this.add(res.data.document);
|
|
|
|
const document = this.data.get(res.data.document.id);
|
|
invariant(document, "Document not available");
|
|
|
|
if (options.shareId) {
|
|
this.sharedCache.set(options.shareId, {
|
|
sharedTree: res.data.sharedTree,
|
|
team: res.data.team,
|
|
});
|
|
return {
|
|
document,
|
|
sharedTree: res.data.sharedTree,
|
|
team: res.data.team,
|
|
};
|
|
}
|
|
|
|
return {
|
|
document,
|
|
};
|
|
} finally {
|
|
this.isFetching = false;
|
|
}
|
|
};
|
|
|
|
@action
|
|
move = async ({
|
|
documentId,
|
|
collectionId,
|
|
parentDocumentId,
|
|
index,
|
|
}: {
|
|
documentId: string;
|
|
collectionId?: string | null;
|
|
parentDocumentId?: string | null;
|
|
index?: number | null;
|
|
}) => {
|
|
this.movingDocumentId = documentId;
|
|
|
|
try {
|
|
const res = await client.post("/documents.move", {
|
|
id: documentId,
|
|
collectionId,
|
|
parentDocumentId,
|
|
index,
|
|
});
|
|
invariant(res?.data, "Data not available");
|
|
res.data.documents.forEach(this.add);
|
|
this.addPolicies(res.policies);
|
|
} finally {
|
|
this.movingDocumentId = undefined;
|
|
}
|
|
};
|
|
|
|
@action
|
|
duplicate = async (
|
|
document: Document,
|
|
options?: {
|
|
title?: string;
|
|
publish?: boolean;
|
|
recursive?: boolean;
|
|
}
|
|
): Promise<Document[]> => {
|
|
const res = await client.post("/documents.duplicate", {
|
|
id: document.id,
|
|
...options,
|
|
});
|
|
invariant(res?.data, "Data should be available");
|
|
|
|
this.addPolicies(res.policies);
|
|
return res.data.documents.map(this.add);
|
|
};
|
|
|
|
@action
|
|
import = async (
|
|
file: File,
|
|
parentDocumentId: string | null | undefined,
|
|
collectionId: string | null | undefined,
|
|
options: ImportOptions
|
|
) => {
|
|
// file.type can be an empty string sometimes
|
|
if (
|
|
file.type &&
|
|
!this.importFileTypes.includes(file.type) &&
|
|
!this.importFileTypes.includes(extname(file.name))
|
|
) {
|
|
throw new Error(`The selected file type is not supported (${file.type})`);
|
|
}
|
|
|
|
if (file.size > env.FILE_STORAGE_IMPORT_MAX_SIZE) {
|
|
throw new Error(
|
|
`The selected file was larger than the ${bytesToHumanReadable(
|
|
env.FILE_STORAGE_IMPORT_MAX_SIZE
|
|
)} maximum size`
|
|
);
|
|
}
|
|
|
|
const title = file.name.replace(/\.[^/.]+$/, "");
|
|
const formData = new FormData();
|
|
[
|
|
{
|
|
key: "parentDocumentId",
|
|
value: parentDocumentId,
|
|
},
|
|
{
|
|
key: "collectionId",
|
|
value: collectionId,
|
|
},
|
|
{
|
|
key: "title",
|
|
value: title,
|
|
},
|
|
{
|
|
key: "publish",
|
|
value: options.publish,
|
|
},
|
|
{
|
|
key: "file",
|
|
value: file,
|
|
},
|
|
].forEach((info) => {
|
|
if (typeof info.value === "string" && info.value) {
|
|
formData.append(info.key, info.value);
|
|
}
|
|
|
|
if (typeof info.value === "boolean") {
|
|
formData.append(info.key, info.value.toString());
|
|
}
|
|
|
|
if (info.value instanceof File) {
|
|
formData.append(info.key, info.value);
|
|
}
|
|
});
|
|
const res = await client.post("/documents.import", formData, {
|
|
retry: false,
|
|
});
|
|
invariant(res?.data, "Data should be available");
|
|
this.addPolicies(res.policies);
|
|
return this.add(res.data);
|
|
};
|
|
|
|
@action
|
|
async delete(
|
|
document: Document,
|
|
options?: {
|
|
permanent: boolean;
|
|
}
|
|
) {
|
|
await super.delete(document, options);
|
|
// check to see if we have any shares related to this document already
|
|
// loaded in local state. If so we can go ahead and remove those too.
|
|
const share = this.rootStore.shares.getByDocumentId(document.id);
|
|
|
|
if (share) {
|
|
this.rootStore.shares.remove(share.id);
|
|
}
|
|
|
|
const collection = this.getCollectionForDocument(document);
|
|
if (collection) {
|
|
await collection.refresh();
|
|
}
|
|
}
|
|
|
|
@action
|
|
archive = async (document: Document) => {
|
|
const res = await client.post("/documents.archive", {
|
|
id: document.id,
|
|
});
|
|
runInAction("Document#archive", () => {
|
|
invariant(res?.data, "Data should be available");
|
|
document.updateData(res.data);
|
|
this.addPolicies(res.policies);
|
|
});
|
|
const collection = this.getCollectionForDocument(document);
|
|
if (collection) {
|
|
await collection.refresh();
|
|
}
|
|
};
|
|
|
|
@action
|
|
restore = async (
|
|
document: Document,
|
|
options: {
|
|
revisionId?: string;
|
|
collectionId?: string;
|
|
} = {}
|
|
) => {
|
|
const res = await client.post("/documents.restore", {
|
|
id: document.id,
|
|
revisionId: options.revisionId,
|
|
collectionId: options.collectionId,
|
|
});
|
|
runInAction("Document#restore", () => {
|
|
invariant(res?.data, "Data should be available");
|
|
document.updateData(res.data);
|
|
this.addPolicies(res.policies);
|
|
});
|
|
const collection = this.getCollectionForDocument(document);
|
|
if (collection) {
|
|
await collection.refresh();
|
|
}
|
|
};
|
|
|
|
@action
|
|
unpublish = async (document: Document) => {
|
|
const res = await client.post("/documents.unpublish", {
|
|
id: document.id,
|
|
});
|
|
|
|
runInAction("Document#unpublish", () => {
|
|
invariant(res?.data, "Data should be available");
|
|
document.updateData(res.data);
|
|
this.addPolicies(res.policies);
|
|
const collection = this.getCollectionForDocument(document);
|
|
void collection?.fetchDocuments({ force: true });
|
|
});
|
|
};
|
|
|
|
@action
|
|
emptyTrash = async () => {
|
|
await client.post("/documents.empty_trash");
|
|
|
|
const documentIdsSet = new Set(this.deleted.map((doc) => doc.id));
|
|
this.removeAll((doc: Document) => documentIdsSet.has(doc.id));
|
|
};
|
|
|
|
star = (document: Document, index?: string) =>
|
|
this.rootStore.stars.create({
|
|
documentId: document.id,
|
|
index,
|
|
});
|
|
|
|
unstar = (document: Document) => {
|
|
const star = this.rootStore.stars.orderedData.find(
|
|
(s) => s.documentId === document.id
|
|
);
|
|
return star?.delete();
|
|
};
|
|
|
|
subscribe = (document: Document) =>
|
|
this.rootStore.subscriptions.create({
|
|
documentId: document.id,
|
|
event: "documents.update",
|
|
});
|
|
|
|
unsubscribe = (userId: string, document: Document) => {
|
|
const subscription = this.rootStore.subscriptions.orderedData.find(
|
|
(s) => s.documentId === document.id && s.userId === userId
|
|
);
|
|
|
|
return subscription?.delete();
|
|
};
|
|
|
|
getByUrl = (url = ""): Document | undefined =>
|
|
find(this.orderedData, (doc) => url.endsWith(doc.urlId));
|
|
|
|
getCollectionForDocument(document: Document) {
|
|
return document.collectionId
|
|
? this.rootStore.collections.get(document.collectionId)
|
|
: undefined;
|
|
}
|
|
}
|