mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f4bc2e36a | |||
| c4b2757403 | |||
| 1f1097250f | |||
| eb2e38addd | |||
| 98687c0c64 | |||
| e5e69838dc | |||
| bf95d4ff6f |
@@ -25,7 +25,7 @@ import User from "~/models/User";
|
||||
import UserMembership from "~/models/UserMembership";
|
||||
import withStores from "~/components/withStores";
|
||||
import {
|
||||
PartialWithId,
|
||||
PartialExcept,
|
||||
WebsocketCollectionUpdateIndexEvent,
|
||||
WebsocketEntitiesEvent,
|
||||
WebsocketEntityDeletedEvent,
|
||||
@@ -214,23 +214,20 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.update",
|
||||
action(
|
||||
(event: PartialWithId<Document> & { title: string; url: string }) => {
|
||||
documents.add(event);
|
||||
action((event: PartialExcept<Document, "id" | "title" | "url">) => {
|
||||
documents.add(event);
|
||||
|
||||
if (event.collectionId) {
|
||||
const collection = collections.get(event.collectionId);
|
||||
collection?.updateDocument(event);
|
||||
}
|
||||
if (event.collectionId) {
|
||||
const collection = collections.get(event.collectionId);
|
||||
collection?.updateDocument(event);
|
||||
}
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
this.socket.on(
|
||||
"documents.archive",
|
||||
action((event: PartialWithId<Document>) => {
|
||||
documents.add(event);
|
||||
policies.remove(event.id);
|
||||
action((event: PartialExcept<Document, "id">) => {
|
||||
documents.addToArchive(event as Document);
|
||||
|
||||
if (event.collectionId) {
|
||||
const collection = collections.get(event.collectionId);
|
||||
@@ -241,7 +238,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.delete",
|
||||
action((event: PartialWithId<Document>) => {
|
||||
action((event: PartialExcept<Document, "id">) => {
|
||||
documents.add(event);
|
||||
policies.remove(event.id);
|
||||
|
||||
@@ -265,7 +262,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.add_user",
|
||||
async (event: PartialWithId<UserMembership>) => {
|
||||
async (event: PartialExcept<UserMembership, "id">) => {
|
||||
userMemberships.add(event);
|
||||
|
||||
// Any existing child policies are now invalid
|
||||
@@ -286,7 +283,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.remove_user",
|
||||
(event: PartialWithId<UserMembership>) => {
|
||||
(event: PartialExcept<UserMembership, "id">) => {
|
||||
userMemberships.remove(event.id);
|
||||
|
||||
// Any existing child policies are now invalid
|
||||
@@ -308,7 +305,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.add_group",
|
||||
(event: PartialWithId<GroupMembership>) => {
|
||||
(event: PartialExcept<GroupMembership, "id">) => {
|
||||
groupMemberships.add(event);
|
||||
|
||||
const group = groups.get(event.groupId!);
|
||||
@@ -330,16 +327,16 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"documents.remove_group",
|
||||
(event: PartialWithId<GroupMembership>) => {
|
||||
(event: PartialExcept<GroupMembership, "id">) => {
|
||||
groupMemberships.remove(event.id);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("comments.create", (event: PartialWithId<Comment>) => {
|
||||
this.socket.on("comments.create", (event: PartialExcept<Comment, "id">) => {
|
||||
comments.add(event);
|
||||
});
|
||||
|
||||
this.socket.on("comments.update", (event: PartialWithId<Comment>) => {
|
||||
this.socket.on("comments.update", (event: PartialExcept<Comment, "id">) => {
|
||||
comments.add(event);
|
||||
});
|
||||
|
||||
@@ -347,11 +344,11 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
comments.remove(event.modelId);
|
||||
});
|
||||
|
||||
this.socket.on("groups.create", (event: PartialWithId<Group>) => {
|
||||
this.socket.on("groups.create", (event: PartialExcept<Group, "id">) => {
|
||||
groups.add(event);
|
||||
});
|
||||
|
||||
this.socket.on("groups.update", (event: PartialWithId<Group>) => {
|
||||
this.socket.on("groups.update", (event: PartialExcept<Group, "id">) => {
|
||||
groups.add(event);
|
||||
});
|
||||
|
||||
@@ -359,24 +356,36 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
groups.remove(event.modelId);
|
||||
});
|
||||
|
||||
this.socket.on("groups.add_user", (event: PartialWithId<GroupUser>) => {
|
||||
groupUsers.add(event);
|
||||
});
|
||||
this.socket.on(
|
||||
"groups.add_user",
|
||||
(event: PartialExcept<GroupUser, "id">) => {
|
||||
groupUsers.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("groups.remove_user", (event: PartialWithId<GroupUser>) => {
|
||||
groupUsers.removeAll({
|
||||
groupId: event.groupId,
|
||||
userId: event.userId,
|
||||
});
|
||||
});
|
||||
this.socket.on(
|
||||
"groups.remove_user",
|
||||
(event: PartialExcept<GroupUser, "id">) => {
|
||||
groupUsers.removeAll({
|
||||
groupId: event.groupId,
|
||||
userId: event.userId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("collections.create", (event: PartialWithId<Collection>) => {
|
||||
collections.add(event);
|
||||
});
|
||||
this.socket.on(
|
||||
"collections.create",
|
||||
(event: PartialExcept<Collection, "id">) => {
|
||||
collections.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("collections.update", (event: PartialWithId<Collection>) => {
|
||||
collections.add(event);
|
||||
});
|
||||
this.socket.on(
|
||||
"collections.update",
|
||||
(event: PartialExcept<Collection, "id">) => {
|
||||
collections.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on(
|
||||
"collections.delete",
|
||||
@@ -398,7 +407,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
})
|
||||
);
|
||||
|
||||
this.socket.on("teams.update", (event: PartialWithId<Team>) => {
|
||||
this.socket.on("teams.update", (event: PartialExcept<Team, "id">) => {
|
||||
if ("sharing" in event && event.sharing !== auth.team?.sharing) {
|
||||
documents.all.forEach((document) => {
|
||||
policies.remove(document.id);
|
||||
@@ -410,23 +419,23 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"notifications.create",
|
||||
(event: PartialWithId<Notification>) => {
|
||||
(event: PartialExcept<Notification, "id">) => {
|
||||
notifications.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on(
|
||||
"notifications.update",
|
||||
(event: PartialWithId<Notification>) => {
|
||||
(event: PartialExcept<Notification, "id">) => {
|
||||
notifications.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("pins.create", (event: PartialWithId<Pin>) => {
|
||||
this.socket.on("pins.create", (event: PartialExcept<Pin, "id">) => {
|
||||
pins.add(event);
|
||||
});
|
||||
|
||||
this.socket.on("pins.update", (event: PartialWithId<Pin>) => {
|
||||
this.socket.on("pins.update", (event: PartialExcept<Pin, "id">) => {
|
||||
pins.add(event);
|
||||
});
|
||||
|
||||
@@ -434,11 +443,11 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
pins.remove(event.modelId);
|
||||
});
|
||||
|
||||
this.socket.on("stars.create", (event: PartialWithId<Star>) => {
|
||||
this.socket.on("stars.create", (event: PartialExcept<Star, "id">) => {
|
||||
stars.add(event);
|
||||
});
|
||||
|
||||
this.socket.on("stars.update", (event: PartialWithId<Star>) => {
|
||||
this.socket.on("stars.update", (event: PartialExcept<Star, "id">) => {
|
||||
stars.add(event);
|
||||
});
|
||||
|
||||
@@ -496,14 +505,14 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"fileOperations.create",
|
||||
(event: PartialWithId<FileOperation>) => {
|
||||
(event: PartialExcept<FileOperation, "id">) => {
|
||||
fileOperations.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on(
|
||||
"fileOperations.update",
|
||||
(event: PartialWithId<FileOperation>) => {
|
||||
(event: PartialExcept<FileOperation, "id">) => {
|
||||
fileOperations.add(event);
|
||||
|
||||
if (
|
||||
@@ -520,7 +529,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"subscriptions.create",
|
||||
(event: PartialWithId<Subscription>) => {
|
||||
(event: PartialExcept<Subscription, "id">) => {
|
||||
subscriptions.add(event);
|
||||
}
|
||||
);
|
||||
@@ -532,11 +541,11 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
}
|
||||
);
|
||||
|
||||
this.socket.on("users.update", (event: PartialWithId<User>) => {
|
||||
this.socket.on("users.update", (event: PartialExcept<User, "id">) => {
|
||||
users.add(event);
|
||||
});
|
||||
|
||||
this.socket.on("users.demote", async (event: PartialWithId<User>) => {
|
||||
this.socket.on("users.demote", async (event: PartialExcept<User, "id">) => {
|
||||
if (event.id === auth.user?.id) {
|
||||
documents.all.forEach((document) => policies.remove(document.id));
|
||||
await collections.fetchAll();
|
||||
@@ -545,7 +554,7 @@ class WebsocketProvider extends React.Component<Props> {
|
||||
|
||||
this.socket.on(
|
||||
"userMemberships.update",
|
||||
async (event: PartialWithId<UserMembership>) => {
|
||||
async (event: PartialExcept<UserMembership, "id">) => {
|
||||
userMemberships.add(event);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { settingsPath } from "~/utils/routeHelpers";
|
||||
import Collection from "./Collection";
|
||||
import Notification from "./Notification";
|
||||
import View from "./View";
|
||||
import ParanoidModel from "./base/ParanoidModel";
|
||||
import ArchivableModel from "./base/ArchivableModel";
|
||||
import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
@@ -37,7 +37,7 @@ type SaveOptions = JSONObject & {
|
||||
autosave?: boolean;
|
||||
};
|
||||
|
||||
export default class Document extends ParanoidModel {
|
||||
export default class Document extends ArchivableModel {
|
||||
static modelName = "Document";
|
||||
|
||||
constructor(fields: Record<string, any>, store: DocumentsStore) {
|
||||
@@ -175,7 +175,10 @@ export default class Document extends ParanoidModel {
|
||||
@observable
|
||||
parentDocumentId: string | undefined;
|
||||
|
||||
@Relation(() => Document)
|
||||
/**
|
||||
* Parent document that this is a child of, if any.
|
||||
*/
|
||||
@Relation(() => Document, { onArchive: "cascade" })
|
||||
parentDocument?: Document;
|
||||
|
||||
@observable
|
||||
@@ -190,9 +193,6 @@ export default class Document extends ParanoidModel {
|
||||
@observable
|
||||
publishedAt: string | undefined;
|
||||
|
||||
@observable
|
||||
archivedAt: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use path instead
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { observable } from "mobx";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
|
||||
export default abstract class ArchivableModel extends ParanoidModel {
|
||||
@observable
|
||||
archivedAt: string | undefined;
|
||||
}
|
||||
@@ -3,12 +3,16 @@ import type Model from "../base/Model";
|
||||
|
||||
/** The behavior of a relationship on deletion */
|
||||
type DeleteBehavior = "cascade" | "null" | "ignore";
|
||||
/** The behavior of a relationship on archival */
|
||||
type ArchiveBehavior = "cascade" | "null" | "ignore";
|
||||
|
||||
type RelationOptions<T = Model> = {
|
||||
/** Whether this relation is required. */
|
||||
required?: boolean;
|
||||
/** Behavior of this model when relationship is deleted. */
|
||||
onDelete: DeleteBehavior | ((item: T) => DeleteBehavior);
|
||||
onDelete?: DeleteBehavior | ((item: T) => DeleteBehavior);
|
||||
/** Behavior of this model when relationship is archived. */
|
||||
onArchive?: ArchiveBehavior | ((item: T) => ArchiveBehavior);
|
||||
};
|
||||
|
||||
type RelationProperties<T = Model> = {
|
||||
|
||||
@@ -11,7 +11,7 @@ import Team from "~/models/Team";
|
||||
import User from "~/models/User";
|
||||
import env from "~/env";
|
||||
import { setPostLoginPath } from "~/hooks/useLastVisitedPath";
|
||||
import { PartialWithId } from "~/types";
|
||||
import { PartialExcept } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import Logger from "~/utils/Logger";
|
||||
@@ -19,8 +19,8 @@ import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import Store from "./base/Store";
|
||||
|
||||
type PersistedData = {
|
||||
user?: PartialWithId<User>;
|
||||
team?: PartialWithId<Team>;
|
||||
user?: PartialExcept<User, "id">;
|
||||
team?: PartialExcept<Team, "id">;
|
||||
collaborationToken?: string;
|
||||
availableTeams?: {
|
||||
id: string;
|
||||
|
||||
@@ -21,7 +21,7 @@ import env from "~/env";
|
||||
import type {
|
||||
FetchOptions,
|
||||
PaginationParams,
|
||||
PartialWithId,
|
||||
PartialExcept,
|
||||
SearchResult,
|
||||
} from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
@@ -489,7 +489,7 @@ export default class DocumentsStore extends Store<Document> {
|
||||
super.fetch(
|
||||
id,
|
||||
options,
|
||||
(res: { data: { document: PartialWithId<Document> } }) =>
|
||||
(res: { data: { document: PartialExcept<Document, "id"> } }) =>
|
||||
res.data.document
|
||||
);
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ import { Pagination } from "@shared/constants";
|
||||
import { type JSONObject } from "@shared/types";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Policy from "~/models/Policy";
|
||||
import ArchivableModel from "~/models/base/ArchivableModel";
|
||||
import Model from "~/models/base/Model";
|
||||
import { LifecycleManager } from "~/models/decorators/Lifecycle";
|
||||
import { getInverseRelationsForModelClass } from "~/models/decorators/Relation";
|
||||
import type { PaginationParams, PartialWithId, Properties } from "~/types";
|
||||
import type { PaginationParams, PartialExcept, Properties } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Logger from "~/utils/Logger";
|
||||
import { AuthorizationError, NotFoundError } from "~/utils/errors";
|
||||
@@ -81,7 +82,7 @@ export default abstract class Store<T extends Model> {
|
||||
};
|
||||
|
||||
@action
|
||||
add = (item: PartialWithId<T> | T): T => {
|
||||
add = (item: PartialExcept<T, "id"> | T): T => {
|
||||
const ModelClass = this.model;
|
||||
|
||||
if (!(item instanceof ModelClass)) {
|
||||
@@ -144,6 +145,42 @@ export default abstract class Store<T extends Model> {
|
||||
LifecycleManager.executeHooks(model.constructor, "afterRemove", model);
|
||||
}
|
||||
|
||||
@action
|
||||
addToArchive(item: ArchivableModel): void {
|
||||
const inverseRelations = getInverseRelationsForModelClass(this.model);
|
||||
|
||||
inverseRelations.forEach((relation) => {
|
||||
const store = this.rootStore.getStoreForModelName(relation.modelName);
|
||||
if ("orderedData" in store) {
|
||||
const items = (store.orderedData as ArchivableModel[]).filter(
|
||||
(data) => data[relation.idKey] === item.id
|
||||
);
|
||||
|
||||
items.forEach((item) => {
|
||||
let archiveBehavior = relation.options.onArchive;
|
||||
|
||||
if (typeof relation.options.onArchive === "function") {
|
||||
archiveBehavior = relation.options.onArchive(item);
|
||||
}
|
||||
|
||||
if (archiveBehavior === "cascade") {
|
||||
store.addToArchive(item);
|
||||
} else if (archiveBehavior === "null") {
|
||||
item[relation.idKey] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove associated policies automatically, not defined through Relation decorator.
|
||||
if (this.modelName !== "Policy") {
|
||||
this.rootStore.policies.remove(item.id);
|
||||
}
|
||||
|
||||
item.archivedAt = new Date().toISOString();
|
||||
(this as unknown as Store<ArchivableModel>).add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all items in the store that match the predicate.
|
||||
*
|
||||
@@ -245,7 +282,7 @@ export default abstract class Store<T extends Model> {
|
||||
async fetch(
|
||||
id: string,
|
||||
options: JSONObject = {},
|
||||
accessor = (res: unknown) => (res as { data: PartialWithId<T> }).data
|
||||
accessor = (res: unknown) => (res as { data: PartialExcept<T, "id"> }).data
|
||||
): Promise<T> {
|
||||
if (!this.actions.includes(RPCAction.Info)) {
|
||||
throw new Error(`Cannot fetch ${this.modelName}`);
|
||||
|
||||
+6
-5
@@ -14,7 +14,8 @@ import Pin from "./models/Pin";
|
||||
import Star from "./models/Star";
|
||||
import UserMembership from "./models/UserMembership";
|
||||
|
||||
export type PartialWithId<T> = Partial<T> & { id: string };
|
||||
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> &
|
||||
Required<Pick<T, K>>;
|
||||
|
||||
export type MenuItemButton = {
|
||||
type: "button";
|
||||
@@ -195,10 +196,10 @@ export type WebsocketCollectionUpdateIndexEvent = {
|
||||
};
|
||||
|
||||
export type WebsocketEvent =
|
||||
| PartialWithId<Pin>
|
||||
| PartialWithId<Star>
|
||||
| PartialWithId<FileOperation>
|
||||
| PartialWithId<UserMembership>
|
||||
| PartialExcept<Pin, "id">
|
||||
| PartialExcept<Star, "id">
|
||||
| PartialExcept<FileOperation, "id">
|
||||
| PartialExcept<UserMembership, "id">
|
||||
| WebsocketCollectionUpdateIndexEvent
|
||||
| WebsocketEntityDeletedEvent
|
||||
| WebsocketEntitiesEvent;
|
||||
|
||||
@@ -64,7 +64,7 @@ import Team from "./Team";
|
||||
import User from "./User";
|
||||
import UserMembership from "./UserMembership";
|
||||
import View from "./View";
|
||||
import ParanoidModel from "./base/ParanoidModel";
|
||||
import ArchivableModel from "./base/ArchivableModel";
|
||||
import Fix from "./decorators/Fix";
|
||||
import { DocumentHelper } from "./helpers/DocumentHelper";
|
||||
import IsHexColor from "./validators/IsHexColor";
|
||||
@@ -259,7 +259,7 @@ type AdditionalFindOptions = {
|
||||
}))
|
||||
@Table({ tableName: "documents", modelName: "document" })
|
||||
@Fix
|
||||
class Document extends ParanoidModel<
|
||||
class Document extends ArchivableModel<
|
||||
InferAttributes<Document>,
|
||||
Partial<InferCreationAttributes<Document>>
|
||||
> {
|
||||
@@ -362,11 +362,6 @@ class Document extends ParanoidModel<
|
||||
@Column(DataType.INTEGER)
|
||||
revisionCount: number;
|
||||
|
||||
/** Whether the document is archvied, and if so when. */
|
||||
@IsDate
|
||||
@Column
|
||||
archivedAt: Date | null;
|
||||
|
||||
/** Whether the document is published, and if so when. */
|
||||
@IsDate
|
||||
@Column
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { AllowNull, Column, IsDate } from "sequelize-typescript";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
|
||||
class ArchivableModel<
|
||||
TModelAttributes extends {} = any,
|
||||
TCreationAttributes extends {} = TModelAttributes
|
||||
> extends ParanoidModel<TModelAttributes, TCreationAttributes> {
|
||||
/** Whether the document is archived, and if so when. */
|
||||
@AllowNull
|
||||
@IsDate
|
||||
@Column
|
||||
archivedAt: Date | null;
|
||||
|
||||
/**
|
||||
* Whether the model has been archived.
|
||||
*
|
||||
* @returns True if the model has been archived
|
||||
*/
|
||||
get isArchived() {
|
||||
return !!this.archivedAt;
|
||||
}
|
||||
}
|
||||
|
||||
export default ArchivableModel;
|
||||
Reference in New Issue
Block a user