mirror of
https://github.com/outline/outline.git
synced 2025-04-15 15:04:27 +00:00
fix: Do not rely on class names in production bundle (#6212)
This commit is contained in:
@ -3,8 +3,7 @@ import { shallow } from "enzyme";
|
||||
import { TFunction } from "i18next";
|
||||
import * as React from "react";
|
||||
import { getI18n } from "react-i18next";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import { runAllPromises } from "~/test/support";
|
||||
import { Component as PaginatedList } from "./PaginatedList";
|
||||
|
||||
@ -12,17 +11,12 @@ describe("PaginatedList", () => {
|
||||
const render = () => null;
|
||||
|
||||
const i18n = getI18n();
|
||||
const { logout, ...store } = new RootStore();
|
||||
|
||||
const props = {
|
||||
i18n,
|
||||
tReady: true,
|
||||
t: ((key: string) => key) as TFunction,
|
||||
logout: () => {
|
||||
//
|
||||
},
|
||||
...store,
|
||||
};
|
||||
} as any;
|
||||
|
||||
it("with no items renders nothing", () => {
|
||||
const list = shallow(
|
||||
@ -59,13 +53,13 @@ describe("PaginatedList", () => {
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith({
|
||||
...options,
|
||||
limit: DEFAULT_PAGINATION_LIMIT,
|
||||
limit: Pagination.defaultLimit,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls fetch when options prop changes", async () => {
|
||||
const fetchedItems = Array(DEFAULT_PAGINATION_LIMIT).fill(undefined);
|
||||
const fetchedItems = Array(Pagination.defaultLimit).fill(undefined);
|
||||
const fetch = jest.fn().mockReturnValue(Promise.resolve(fetchedItems));
|
||||
const list = shallow(
|
||||
<PaginatedList
|
||||
@ -81,7 +75,7 @@ describe("PaginatedList", () => {
|
||||
await runAllPromises();
|
||||
expect(fetch).toHaveBeenCalledWith({
|
||||
id: "one",
|
||||
limit: DEFAULT_PAGINATION_LIMIT,
|
||||
limit: Pagination.defaultLimit,
|
||||
offset: 0,
|
||||
});
|
||||
fetch.mockReset();
|
||||
@ -95,7 +89,7 @@ describe("PaginatedList", () => {
|
||||
await runAllPromises();
|
||||
expect(fetch).toHaveBeenCalledWith({
|
||||
id: "two",
|
||||
limit: DEFAULT_PAGINATION_LIMIT,
|
||||
limit: Pagination.defaultLimit,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
@ -5,8 +5,8 @@ import * as React from "react";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import { CompositeStateReturn } from "reakit/Composite";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
import DelayedMount from "~/components/DelayedMount";
|
||||
import PlaceholderList from "~/components/List/Placeholder";
|
||||
@ -86,7 +86,7 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
reset = () => {
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
this.renderCount = DEFAULT_PAGINATION_LIMIT;
|
||||
this.renderCount = Pagination.defaultLimit;
|
||||
this.isFetching = false;
|
||||
this.isFetchingInitial = false;
|
||||
this.isFetchingMore = false;
|
||||
@ -99,7 +99,7 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
}
|
||||
this.isFetching = true;
|
||||
const counter = ++this.fetchCounter;
|
||||
const limit = this.props.options?.limit ?? DEFAULT_PAGINATION_LIMIT;
|
||||
const limit = this.props.options?.limit ?? Pagination.defaultLimit;
|
||||
this.error = undefined;
|
||||
|
||||
try {
|
||||
@ -139,12 +139,12 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
const leftToRender = (this.props.items?.length ?? 0) - this.renderCount;
|
||||
|
||||
if (leftToRender > 0) {
|
||||
this.renderCount += DEFAULT_PAGINATION_LIMIT;
|
||||
this.renderCount += Pagination.defaultLimit;
|
||||
}
|
||||
|
||||
// If there are less than a pages results in the cache go ahead and fetch
|
||||
// another page from the server
|
||||
if (leftToRender <= DEFAULT_PAGINATION_LIMIT) {
|
||||
if (leftToRender <= Pagination.defaultLimit) {
|
||||
this.isFetchingMore = true;
|
||||
await this.fetchResults();
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class ApiKey extends Model {
|
||||
static modelName = "ApiKey";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -3,6 +3,8 @@ import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class AuthenticationProvider extends Model {
|
||||
static modelName = "AuthenticationProvider";
|
||||
|
||||
id: string;
|
||||
|
||||
displayName: string;
|
||||
|
@ -13,6 +13,8 @@ import { client } from "~/utils/ApiClient";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
export default class Collection extends ParanoidModel {
|
||||
static modelName = "Collection";
|
||||
|
||||
store: CollectionsStore;
|
||||
|
||||
@observable
|
||||
|
@ -1,14 +1,25 @@
|
||||
import { observable } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "./Collection";
|
||||
import Group from "./Group";
|
||||
import Model from "./base/Model";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class CollectionGroupMembership extends Model {
|
||||
static modelName = "CollectionGroupMembership";
|
||||
|
||||
id: string;
|
||||
|
||||
groupId: string;
|
||||
|
||||
@Relation(() => Group, { onDelete: "cascade" })
|
||||
group: Group;
|
||||
|
||||
collectionId: string;
|
||||
|
||||
@Relation(() => Collection, { onDelete: "cascade" })
|
||||
collection: Collection;
|
||||
|
||||
@observable
|
||||
permission: CollectionPermission;
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Comment extends Model {
|
||||
static modelName = "Comment";
|
||||
|
||||
/**
|
||||
* Map to keep track of which users are currently typing a reply in this
|
||||
* comments thread.
|
||||
|
@ -22,6 +22,8 @@ type SaveOptions = {
|
||||
};
|
||||
|
||||
export default class Document extends ParanoidModel {
|
||||
static modelName = "Document";
|
||||
|
||||
constructor(fields: Record<string, any>, store: DocumentsStore) {
|
||||
super(fields, store);
|
||||
|
||||
|
@ -3,6 +3,8 @@ import Model from "./base/Model";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Event extends Model {
|
||||
static modelName = "Event";
|
||||
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
|
@ -5,6 +5,8 @@ import User from "./User";
|
||||
import Model from "./base/Model";
|
||||
|
||||
class FileOperation extends Model {
|
||||
static modelName = "FileOperation";
|
||||
|
||||
id: string;
|
||||
|
||||
@observable
|
||||
|
@ -3,6 +3,8 @@ import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class Group extends Model {
|
||||
static modelName = "Group";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -1,16 +1,20 @@
|
||||
import Group from "./Group";
|
||||
import User from "./User";
|
||||
import Model from "./base/Model";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class GroupMembership extends Model {
|
||||
id: string;
|
||||
static modelName = "GroupMembership";
|
||||
|
||||
userId: string;
|
||||
|
||||
groupId: string;
|
||||
|
||||
@Relation(() => User, { onDelete: "cascade" })
|
||||
user: User;
|
||||
|
||||
groupId: string;
|
||||
|
||||
@Relation(() => Group, { onDelete: "cascade" })
|
||||
group: Group;
|
||||
}
|
||||
|
||||
export default GroupMembership;
|
||||
|
@ -8,6 +8,8 @@ import Model from "~/models/base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class Integration<T = unknown> extends Model {
|
||||
static modelName = "Integration";
|
||||
|
||||
id: string;
|
||||
|
||||
type: IntegrationType;
|
||||
|
@ -3,6 +3,8 @@ import { CollectionPermission } from "@shared/types";
|
||||
import Model from "./base/Model";
|
||||
|
||||
class Membership extends Model {
|
||||
static modelName = "Membership";
|
||||
|
||||
id: string;
|
||||
|
||||
userId: string;
|
||||
|
@ -15,6 +15,8 @@ import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Notification extends Model {
|
||||
static modelName = "Notification";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -6,6 +6,8 @@ import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Pin extends Model {
|
||||
static modelName = "Pin";
|
||||
|
||||
/** The collection ID that the document is pinned to. If empty the document is pinned to home. */
|
||||
collectionId: string;
|
||||
|
||||
|
@ -2,6 +2,8 @@ import { observable } from "mobx";
|
||||
import Model from "./base/Model";
|
||||
|
||||
class Policy extends Model {
|
||||
static modelName = "Policy";
|
||||
|
||||
id: string;
|
||||
|
||||
@observable
|
||||
|
@ -6,6 +6,8 @@ import Model from "./base/Model";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Revision extends Model {
|
||||
static modelName = "Revision";
|
||||
|
||||
/** The document ID that the revision is related to */
|
||||
documentId: string;
|
||||
|
||||
|
@ -2,6 +2,8 @@ import { client } from "~/utils/ApiClient";
|
||||
import Model from "./base/Model";
|
||||
|
||||
class SearchQuery extends Model {
|
||||
static modelName = "Search";
|
||||
|
||||
id: string;
|
||||
|
||||
query: string;
|
||||
|
@ -6,6 +6,8 @@ import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Share extends Model {
|
||||
static modelName = "Share";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
published: boolean;
|
||||
|
@ -7,6 +7,8 @@ import Field from "./decorators/Field";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class Star extends Model {
|
||||
static modelName = "Star";
|
||||
|
||||
/** The sort order of the star */
|
||||
@Field
|
||||
@observable
|
||||
|
@ -9,6 +9,8 @@ import Relation from "./decorators/Relation";
|
||||
* A subscription represents a request for a user to receive notifications for a document.
|
||||
*/
|
||||
class Subscription extends Model {
|
||||
static modelName = "Subscription";
|
||||
|
||||
/** The user ID subscribing */
|
||||
userId: string;
|
||||
|
||||
|
@ -6,6 +6,8 @@ import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class Team extends Model {
|
||||
static modelName = "Team";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -16,6 +16,8 @@ import ParanoidModel from "./base/ParanoidModel";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class User extends ParanoidModel {
|
||||
static modelName = "User";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { action, observable } from "mobx";
|
||||
import Document from "./Document";
|
||||
import User from "./User";
|
||||
import Model from "./base/Model";
|
||||
import Relation from "./decorators/Relation";
|
||||
|
||||
class View extends Model {
|
||||
static modelName = "View";
|
||||
|
||||
id: string;
|
||||
|
||||
documentId: string;
|
||||
|
||||
@Relation(() => Document)
|
||||
document?: Document;
|
||||
|
||||
firstViewedAt: string;
|
||||
|
||||
@observable
|
||||
|
@ -3,6 +3,8 @@ import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class WebhookSubscription extends Model {
|
||||
static modelName = "WebhookSubscription";
|
||||
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
|
@ -5,6 +5,8 @@ import Logger from "~/utils/Logger";
|
||||
import { getFieldsForModel } from "../decorators/Field";
|
||||
|
||||
export default abstract class Model {
|
||||
static modelName: string;
|
||||
|
||||
@observable
|
||||
id: string;
|
||||
|
||||
|
@ -35,7 +35,9 @@ export const getInverseRelationsForModelClass = (targetClass: typeof Model) => {
|
||||
|
||||
relations.forEach((relation, modelName) => {
|
||||
relation.forEach((properties, propertyName) => {
|
||||
if (properties.relationClassResolver().name === targetClass.name) {
|
||||
if (
|
||||
properties.relationClassResolver().modelName === targetClass.modelName
|
||||
) {
|
||||
inverseRelations.set(propertyName, {
|
||||
...properties,
|
||||
modelName,
|
||||
@ -66,13 +68,13 @@ export default function Relation<T extends typeof Model>(
|
||||
// this to determine how to update relations when a model is deleted.
|
||||
if (options) {
|
||||
const configForClass =
|
||||
relations.get(target.constructor.name) || new Map();
|
||||
relations.get(target.constructor.modelName) || new Map();
|
||||
configForClass.set(propertyKey, {
|
||||
options,
|
||||
relationClassResolver: classResolver,
|
||||
idKey,
|
||||
});
|
||||
relations.set(target.constructor.name, configForClass);
|
||||
relations.set(target.constructor.modelName, configForClass);
|
||||
}
|
||||
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
@ -83,9 +85,9 @@ export default function Relation<T extends typeof Model>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relationClassName = classResolver().name;
|
||||
const relationClassName = classResolver().modelName;
|
||||
const store =
|
||||
this.store.rootStore[`${relationClassName.toLowerCase()}s`];
|
||||
this.store.rootStore.getStoreForModelName(relationClassName);
|
||||
invariant(store, `Store for ${relationClassName} not found`);
|
||||
|
||||
return store.get(id);
|
||||
@ -94,9 +96,9 @@ export default function Relation<T extends typeof Model>(
|
||||
this[idKey] = newValue ? newValue.id : undefined;
|
||||
|
||||
if (newValue) {
|
||||
const relationClassName = classResolver().name;
|
||||
const relationClassName = classResolver().modelName;
|
||||
const store =
|
||||
this.store.rootStore[`${relationClassName.toLowerCase()}s`];
|
||||
this.store.rootStore.getStoreForModelName(relationClassName);
|
||||
invariant(store, `Store for ${relationClassName} not found`);
|
||||
|
||||
store.add(newValue);
|
||||
|
@ -9,11 +9,11 @@ import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import { hideScrollbars } from "@shared/styles";
|
||||
import { DateFilter as TDateFilter } from "@shared/types";
|
||||
import { SearchParams } from "~/stores/DocumentsStore";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
import DocumentListItem from "~/components/DocumentListItem";
|
||||
import Empty from "~/components/Empty";
|
||||
@ -248,7 +248,7 @@ class Search extends React.Component<Props> {
|
||||
if (this.query.trim()) {
|
||||
const params = {
|
||||
offset: this.offset,
|
||||
limit: DEFAULT_PAGINATION_LIMIT,
|
||||
limit: Pagination.defaultLimit,
|
||||
dateFilter: this.dateFilter,
|
||||
includeArchived: this.includeArchived,
|
||||
includeDrafts: true,
|
||||
@ -280,10 +280,10 @@ class Search extends React.Component<Props> {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) {
|
||||
if (results.length === 0 || results.length < Pagination.defaultLimit) {
|
||||
this.allowLoadMore = false;
|
||||
} else {
|
||||
this.offset += DEFAULT_PAGINATION_LIMIT;
|
||||
this.offset += Pagination.defaultLimit;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Search query failed", error);
|
||||
|
@ -351,6 +351,6 @@ export default class AuthStore extends Store<Team> {
|
||||
|
||||
// Tell the host application we logged out, if any – allows window cleanup.
|
||||
void Desktop.bridge?.onLogout?.();
|
||||
this.rootStore.logout();
|
||||
this.rootStore.clear();
|
||||
};
|
||||
}
|
||||
|
@ -10,8 +10,6 @@ import RootStore from "./RootStore";
|
||||
import Store from "./base/Store";
|
||||
|
||||
export default class CommentsStore extends Store<Comment> {
|
||||
apiEndpoint = "comments";
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, Comment);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import invariant from "invariant";
|
||||
import pluralize from "pluralize";
|
||||
import ApiKeysStore from "./ApiKeysStore";
|
||||
import AuthStore from "./AuthStore";
|
||||
import AuthenticationProvidersStore from "./AuthenticationProvidersStore";
|
||||
@ -25,6 +27,7 @@ import UiStore from "./UiStore";
|
||||
import UsersStore from "./UsersStore";
|
||||
import ViewsStore from "./ViewsStore";
|
||||
import WebhookSubscriptionsStore from "./WebhookSubscriptionStore";
|
||||
import Store from "./base/Store";
|
||||
|
||||
export default class RootStore {
|
||||
apiKeys: ApiKeysStore;
|
||||
@ -56,42 +59,79 @@ export default class RootStore {
|
||||
webhookSubscriptions: WebhookSubscriptionsStore;
|
||||
|
||||
constructor() {
|
||||
this.apiKeys = new ApiKeysStore(this);
|
||||
this.authenticationProviders = new AuthenticationProvidersStore(this);
|
||||
this.collections = new CollectionsStore(this);
|
||||
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
|
||||
this.comments = new CommentsStore(this);
|
||||
this.dialogs = new DialogsStore();
|
||||
this.documents = new DocumentsStore(this);
|
||||
this.events = new EventsStore(this);
|
||||
this.groups = new GroupsStore(this);
|
||||
this.groupMemberships = new GroupMembershipsStore(this);
|
||||
this.integrations = new IntegrationsStore(this);
|
||||
this.memberships = new MembershipsStore(this);
|
||||
this.notifications = new NotificationsStore(this);
|
||||
this.pins = new PinsStore(this);
|
||||
this.policies = new PoliciesStore(this);
|
||||
this.presence = new DocumentPresenceStore();
|
||||
this.revisions = new RevisionsStore(this);
|
||||
this.searches = new SearchesStore(this);
|
||||
this.shares = new SharesStore(this);
|
||||
this.stars = new StarsStore(this);
|
||||
this.subscriptions = new SubscriptionsStore(this);
|
||||
this.ui = new UiStore();
|
||||
this.users = new UsersStore(this);
|
||||
this.views = new ViewsStore(this);
|
||||
this.fileOperations = new FileOperationsStore(this);
|
||||
this.webhookSubscriptions = new WebhookSubscriptionsStore(this);
|
||||
// Models
|
||||
this.registerStore(ApiKeysStore);
|
||||
this.registerStore(AuthenticationProvidersStore);
|
||||
this.registerStore(CollectionsStore);
|
||||
this.registerStore(CollectionGroupMembershipsStore);
|
||||
this.registerStore(CommentsStore);
|
||||
this.registerStore(DocumentsStore);
|
||||
this.registerStore(EventsStore);
|
||||
this.registerStore(GroupsStore);
|
||||
this.registerStore(GroupMembershipsStore);
|
||||
this.registerStore(IntegrationsStore);
|
||||
this.registerStore(MembershipsStore);
|
||||
this.registerStore(NotificationsStore);
|
||||
this.registerStore(PinsStore);
|
||||
this.registerStore(PoliciesStore);
|
||||
this.registerStore(RevisionsStore);
|
||||
this.registerStore(SearchesStore);
|
||||
this.registerStore(SharesStore);
|
||||
this.registerStore(StarsStore);
|
||||
this.registerStore(SubscriptionsStore);
|
||||
this.registerStore(UsersStore);
|
||||
this.registerStore(ViewsStore);
|
||||
this.registerStore(FileOperationsStore);
|
||||
this.registerStore(WebhookSubscriptionsStore);
|
||||
|
||||
// Non-models
|
||||
this.registerStore(DocumentPresenceStore, "presence");
|
||||
this.registerStore(DialogsStore, "dialogs");
|
||||
this.registerStore(UiStore, "ui");
|
||||
|
||||
// AuthStore must be initialized last as it makes use of the other stores.
|
||||
this.auth = new AuthStore(this);
|
||||
this.registerStore(AuthStore, "auth");
|
||||
}
|
||||
|
||||
logout() {
|
||||
/**
|
||||
* Get a store by model name.
|
||||
*
|
||||
* @param modelName
|
||||
*/
|
||||
public getStoreForModelName<K extends keyof RootStore>(
|
||||
modelName: string
|
||||
): RootStore[K] {
|
||||
const storeName = this.getStoreNameForModelName(modelName);
|
||||
const store = this[storeName];
|
||||
invariant(store, `No store found for model name "${modelName}"`);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data from the stores except for auth and ui.
|
||||
*/
|
||||
public clear() {
|
||||
Object.getOwnPropertyNames(this)
|
||||
.filter((key) => ["auth", "ui"].includes(key) === false)
|
||||
.forEach((key) => {
|
||||
this[key]?.clear?.();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a store with the root store.
|
||||
*
|
||||
* @param StoreClass
|
||||
*/
|
||||
private registerStore<T = typeof Store>(StoreClass: T, name?: string) {
|
||||
// @ts-expect-error TS thinks we are instantiating an abstract class.
|
||||
const store = new StoreClass(this);
|
||||
const storeName = name ?? this.getStoreNameForModelName(store.modelName);
|
||||
this[storeName] = store;
|
||||
}
|
||||
|
||||
private getStoreNameForModelName(modelName: string) {
|
||||
return pluralize(modelName.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ import Store, { RPCAction } from "./base/Store";
|
||||
export default class SearchesStore extends Store<SearchQuery> {
|
||||
actions = [RPCAction.List, RPCAction.Delete];
|
||||
|
||||
apiEndpoint = "searches";
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, SearchQuery);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import invariant from "invariant";
|
||||
import lowerFirst from "lodash/lowerFirst";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { observable, action, computed, runInAction } from "mobx";
|
||||
import { Class } from "utility-types";
|
||||
import pluralize from "pluralize";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Policy from "~/models/Policy";
|
||||
import Model from "~/models/base/Model";
|
||||
@ -22,8 +22,6 @@ export enum RPCAction {
|
||||
|
||||
type FetchPageParams = PaginationParams & Record<string, any>;
|
||||
|
||||
export const DEFAULT_PAGINATION_LIMIT = 25;
|
||||
|
||||
export const PAGINATION_SYMBOL = Symbol.for("pagination");
|
||||
|
||||
export default abstract class Store<T extends Model> {
|
||||
@ -39,7 +37,7 @@ export default abstract class Store<T extends Model> {
|
||||
@observable
|
||||
isLoaded = false;
|
||||
|
||||
model: Class<T>;
|
||||
model: typeof Model;
|
||||
|
||||
modelName: string;
|
||||
|
||||
@ -56,13 +54,13 @@ export default abstract class Store<T extends Model> {
|
||||
RPCAction.Count,
|
||||
];
|
||||
|
||||
constructor(rootStore: RootStore, model: Class<T>) {
|
||||
constructor(rootStore: RootStore, model: typeof Model) {
|
||||
this.rootStore = rootStore;
|
||||
this.model = model;
|
||||
this.modelName = lowerFirst(model.name).replace(/\d$/, "");
|
||||
this.modelName = model.modelName;
|
||||
|
||||
if (!this.apiEndpoint) {
|
||||
this.apiEndpoint = `${this.modelName}s`;
|
||||
this.apiEndpoint = pluralize(lowerFirst(model.modelName));
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +87,7 @@ export default abstract class Store<T extends Model> {
|
||||
return existingModel;
|
||||
}
|
||||
|
||||
// @ts-expect-error TS thinks that we're instantiating an abstract class here
|
||||
const newModel = new ModelClass(item, this);
|
||||
this.data.set(newModel.id, newModel);
|
||||
return newModel;
|
||||
@ -103,20 +102,21 @@ export default abstract class Store<T extends Model> {
|
||||
const inverseRelations = getInverseRelationsForModelClass(this.model);
|
||||
|
||||
inverseRelations.forEach((relation) => {
|
||||
// TODO: Need a better way to get the store for a given model name.
|
||||
const store = this.rootStore[`${relation.modelName.toLowerCase()}s`];
|
||||
const items = store.orderedData.filter(
|
||||
(item: Model) => item[relation.idKey] === id
|
||||
);
|
||||
const store = this.rootStore.getStoreForModelName(relation.modelName);
|
||||
if ("orderedData" in store) {
|
||||
const items = (store.orderedData as Model[]).filter(
|
||||
(item) => item[relation.idKey] === id
|
||||
);
|
||||
|
||||
if (relation.options.onDelete === "cascade") {
|
||||
items.forEach((item: Model) => store.remove(item.id));
|
||||
}
|
||||
if (relation.options.onDelete === "cascade") {
|
||||
items.forEach((item) => store.remove(item.id));
|
||||
}
|
||||
|
||||
if (relation.options.onDelete === "null") {
|
||||
items.forEach((item: Model) => {
|
||||
item[relation.idKey] = null;
|
||||
});
|
||||
if (relation.options.onDelete === "null") {
|
||||
items.forEach((item) => {
|
||||
item[relation.idKey] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -157,6 +157,7 @@
|
||||
"patch-package": "^7.0.2",
|
||||
"pg": "^8.11.1",
|
||||
"pg-tsquery": "^8.4.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"polished": "^4.2.2",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-commands": "^1.5.2",
|
||||
@ -278,6 +279,7 @@
|
||||
"@types/node-fetch": "^2.6.9",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-oauth2": "^1.4.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/quoted-printable": "^1.0.0",
|
||||
"@types/randomstring": "^1.1.11",
|
||||
"@types/react": "^17.0.34",
|
||||
|
@ -1,14 +1,15 @@
|
||||
import querystring from "querystring";
|
||||
import { Next } from "koa";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import { InvalidRequestError } from "@server/errors";
|
||||
import { AppContext } from "@server/types";
|
||||
|
||||
export default function pagination() {
|
||||
return async function paginationMiddleware(ctx: AppContext, next: Next) {
|
||||
const opts = {
|
||||
defaultLimit: 15,
|
||||
defaultOffset: 0,
|
||||
maxLimit: 100,
|
||||
defaultLimit: Pagination.defaultLimit,
|
||||
defaultOffset: Pagination.defaultOffset,
|
||||
maxLimit: Pagination.maxLimit,
|
||||
};
|
||||
const query = ctx.request.query;
|
||||
const body = ctx.request.body;
|
||||
|
@ -7,6 +7,12 @@ import {
|
||||
|
||||
export const MAX_AVATAR_DISPLAY = 6;
|
||||
|
||||
export const Pagination = {
|
||||
defaultLimit: 25,
|
||||
defaultOffset: 0,
|
||||
maxLimit: 100,
|
||||
};
|
||||
|
||||
export const TeamPreferenceDefaults: TeamPreferences = {
|
||||
[TeamPreference.SeamlessEdit]: true,
|
||||
[TeamPreference.ViewersCanExport]: true,
|
||||
|
10
yarn.lock
10
yarn.lock
@ -3314,6 +3314,11 @@
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/pluralize@^0.0.33":
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.33.tgz#8ad9018368c584d268667dd9acd5b3b806e8c82a"
|
||||
integrity sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==
|
||||
|
||||
"@types/prismjs@*":
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
|
||||
@ -10609,6 +10614,11 @@ pkg-up@^3.1.0:
|
||||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
pluralize@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||
|
||||
polished@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
||||
|
Reference in New Issue
Block a user