fix: Flash of empty state on paginated lists (#3351)
* fix: Flash of empty state on paginated lists fix: Typing of PaginatedList to generic * test * test
This commit is contained in:
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Document from "~/models/Document";
|
||||
import User from "~/models/User";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import PaginatedList from "~/components/PaginatedList";
|
||||
@@ -42,7 +43,7 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
<PaginatedList
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={(item) => {
|
||||
renderItem={(item: User) => {
|
||||
const view = documentViews.find((v) => v.user.id === item.id);
|
||||
const isPresent = presentIds.includes(item.id);
|
||||
const isEditing = editingIds.includes(item.id);
|
||||
|
||||
@@ -6,7 +6,7 @@ import PaginatedList from "~/components/PaginatedList";
|
||||
|
||||
type Props = {
|
||||
documents: Document[];
|
||||
fetch: (options: any) => Promise<void>;
|
||||
fetch: (options: any) => Promise<Document[] | undefined>;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
@@ -40,7 +40,7 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
|
||||
heading={heading}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={(item, _index, compositeProps) => (
|
||||
renderItem={(item: Document, _index, compositeProps) => (
|
||||
<DocumentListItem
|
||||
key={item.id}
|
||||
document={item}
|
||||
|
||||
@@ -8,7 +8,7 @@ import EventListItem from "./EventListItem";
|
||||
type Props = {
|
||||
events: Event[];
|
||||
document: Document;
|
||||
fetch: (options: Record<string, any> | null | undefined) => Promise<void>;
|
||||
fetch: (options: Record<string, any> | undefined) => Promise<Event[]>;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
@@ -29,7 +29,7 @@ const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
|
||||
heading={heading}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={(item, index, compositeProps) => {
|
||||
renderItem={(item: Event, index, compositeProps) => {
|
||||
return (
|
||||
<EventListItem
|
||||
key={item.id}
|
||||
|
||||
@@ -13,18 +13,24 @@ import PlaceholderList from "~/components/List/Placeholder";
|
||||
import withStores from "~/components/withStores";
|
||||
import { dateToHeading } from "~/utils/dates";
|
||||
|
||||
type Props = WithTranslation &
|
||||
export interface PaginatedItem {
|
||||
id: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
type Props<T> = WithTranslation &
|
||||
RootStore & {
|
||||
fetch?: (
|
||||
options: Record<string, any> | null | undefined
|
||||
) => Promise<any> | undefined;
|
||||
options: Record<string, any> | undefined
|
||||
) => Promise<T[] | undefined> | undefined;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
loading?: React.ReactElement;
|
||||
items?: any[];
|
||||
items?: T[];
|
||||
renderItem: (
|
||||
item: any,
|
||||
item: T,
|
||||
index: number,
|
||||
compositeProps: CompositeStateReturn
|
||||
) => React.ReactNode;
|
||||
@@ -33,13 +39,14 @@ type Props = WithTranslation &
|
||||
};
|
||||
|
||||
@observer
|
||||
class PaginatedList extends React.Component<Props> {
|
||||
class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
@observable
|
||||
isFetchingMore = false;
|
||||
|
||||
@observable
|
||||
isFetching = false;
|
||||
|
||||
@observable
|
||||
fetchCounter = 0;
|
||||
|
||||
@observable
|
||||
@@ -55,7 +62,7 @@ class PaginatedList extends React.Component<Props> {
|
||||
this.fetchResults();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate(prevProps: Props<T>) {
|
||||
if (
|
||||
prevProps.fetch !== this.props.fetch ||
|
||||
!isEqual(prevProps.options, this.props.options)
|
||||
@@ -125,76 +132,82 @@ class PaginatedList extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items, heading, auth, empty, renderHeading, onEscape } = this.props;
|
||||
const {
|
||||
items = [],
|
||||
heading,
|
||||
auth,
|
||||
empty = null,
|
||||
renderHeading,
|
||||
onEscape,
|
||||
} = this.props;
|
||||
let previousHeading = "";
|
||||
|
||||
const showList = !!items?.length;
|
||||
const showEmpty = items?.length === 0;
|
||||
const showLoading =
|
||||
this.isFetching && !this.isFetchingMore && !showList && !showEmpty;
|
||||
this.isFetching &&
|
||||
!this.isFetchingMore &&
|
||||
(!items?.length || this.fetchCounter === 0);
|
||||
|
||||
if (showLoading) {
|
||||
return (
|
||||
this.props.loading || (
|
||||
<DelayedMount>
|
||||
<PlaceholderList count={5} />
|
||||
</DelayedMount>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (items?.length === 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showEmpty && empty}
|
||||
{showList && (
|
||||
<>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
aria-label={this.props["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
>
|
||||
{(composite: CompositeStateReturn) =>
|
||||
items.slice(0, this.renderCount).map((item, index) => {
|
||||
const children = this.props.renderItem(
|
||||
item,
|
||||
index,
|
||||
composite
|
||||
);
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
aria-label={this.props["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
>
|
||||
{(composite: CompositeStateReturn) =>
|
||||
items.slice(0, this.renderCount).map((item, index) => {
|
||||
const children = this.props.renderItem(item, index, composite);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
if (!renderHeading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Our models have standard date fields, updatedAt > createdAt.
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
item.updatedAt || item.createdAt || previousHeading;
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
this.props.t,
|
||||
auth.user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
// heading
|
||||
if (!previousHeading || currentHeading !== previousHeading) {
|
||||
previousHeading = currentHeading;
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{renderHeading(currentHeading)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
})
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
if (!renderHeading) {
|
||||
return children;
|
||||
}
|
||||
</ArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
</>
|
||||
|
||||
// Our models have standard date fields, updatedAt > createdAt.
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
item.updatedAt || item.createdAt || previousHeading;
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
this.props.t,
|
||||
auth.user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
// heading
|
||||
if (!previousHeading || currentHeading !== previousHeading) {
|
||||
previousHeading = currentHeading;
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{renderHeading(currentHeading)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
})
|
||||
}
|
||||
</ArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
{showLoading &&
|
||||
(this.props.loading || (
|
||||
<DelayedMount>
|
||||
<PlaceholderList count={5} />
|
||||
</DelayedMount>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import Empty from "~/components/Empty";
|
||||
import { Outline } from "~/components/Input";
|
||||
import InputSearch from "~/components/InputSearch";
|
||||
import Placeholder from "~/components/List/Placeholder";
|
||||
import PaginatedList from "~/components/PaginatedList";
|
||||
import PaginatedList, { PaginatedItem } from "~/components/PaginatedList";
|
||||
import Popover from "~/components/Popover";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { SearchResult } from "~/types";
|
||||
import SearchListItem from "./SearchListItem";
|
||||
|
||||
type Props = { shareId: string };
|
||||
@@ -31,7 +32,7 @@ function SearchPopover({ shareId }: Props) {
|
||||
|
||||
const [cachedQuery, setCachedQuery] = React.useState(query);
|
||||
const [cachedSearchResults, setCachedSearchResults] = React.useState<
|
||||
Record<string, any>[] | undefined
|
||||
PaginatedItem[] | undefined
|
||||
>(searchResults);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -43,7 +44,7 @@ function SearchPopover({ shareId }: Props) {
|
||||
}, [searchResults, query, popover.show]);
|
||||
|
||||
const performSearch = React.useCallback(
|
||||
async ({ query, ...options }: Record<string, any>) => {
|
||||
async ({ query, ...options }) => {
|
||||
if (query?.length > 0) {
|
||||
return await documents.search(query, { shareId, ...options });
|
||||
}
|
||||
@@ -161,7 +162,7 @@ function SearchPopover({ shareId }: Props) {
|
||||
<NoResults>{t("No results for {{query}}", { query })}</NoResults>
|
||||
}
|
||||
loading={<PlaceholderList count={3} header={{ height: 20 }} />}
|
||||
renderItem={(item, index, compositeProps) => (
|
||||
renderItem={(item: SearchResult, index, compositeProps) => (
|
||||
<SearchListItem
|
||||
key={item.document.id}
|
||||
shareId={shareId}
|
||||
|
||||
@@ -2,13 +2,17 @@ import { pick } from "lodash";
|
||||
import { set, computed, observable } from "mobx";
|
||||
import { getFieldsForModel } from "./decorators/Field";
|
||||
|
||||
export default class BaseModel {
|
||||
export default abstract class BaseModel {
|
||||
@observable
|
||||
id: string;
|
||||
|
||||
@observable
|
||||
isSaving: boolean;
|
||||
|
||||
createdAt: string;
|
||||
|
||||
updatedAt: string;
|
||||
|
||||
store: any;
|
||||
|
||||
constructor(fields: Record<string, any>, store: any) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { trim } from "lodash";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import CollectionsStore from "~/stores/CollectionsStore";
|
||||
import BaseModel from "~/models/BaseModel";
|
||||
import Document from "~/models/Document";
|
||||
import ParanoidModel from "~/models/ParanoidModel";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
export default class Collection extends BaseModel {
|
||||
export default class Collection extends ParanoidModel {
|
||||
store: CollectionsStore;
|
||||
|
||||
@observable
|
||||
@@ -57,12 +57,6 @@ export default class Collection extends BaseModel {
|
||||
|
||||
documents: NavigationNode[];
|
||||
|
||||
createdAt: string;
|
||||
|
||||
updatedAt: string;
|
||||
|
||||
deletedAt: string | null | undefined;
|
||||
|
||||
url: string;
|
||||
|
||||
urlId: string;
|
||||
|
||||
@@ -4,9 +4,9 @@ import { action, computed, observable } from "mobx";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import unescape from "@shared/utils/unescape";
|
||||
import DocumentsStore from "~/stores/DocumentsStore";
|
||||
import BaseModel from "~/models/BaseModel";
|
||||
import User from "~/models/User";
|
||||
import { NavigationNode } from "~/types";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
import View from "./View";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
@@ -17,7 +17,7 @@ type SaveOptions = {
|
||||
lastRevision?: number;
|
||||
};
|
||||
|
||||
export default class Document extends BaseModel {
|
||||
export default class Document extends ParanoidModel {
|
||||
@observable
|
||||
isSaving = false;
|
||||
|
||||
@@ -63,20 +63,14 @@ export default class Document extends BaseModel {
|
||||
|
||||
collaboratorIds: string[];
|
||||
|
||||
createdAt: string;
|
||||
|
||||
createdBy: User;
|
||||
|
||||
updatedAt: string;
|
||||
|
||||
updatedBy: User;
|
||||
|
||||
publishedAt: string | undefined;
|
||||
|
||||
archivedAt: string;
|
||||
|
||||
deletedAt: string | undefined;
|
||||
|
||||
url: string;
|
||||
|
||||
urlId: string;
|
||||
|
||||
5
app/models/ParanoidModel.ts
Normal file
5
app/models/ParanoidModel.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
export default abstract class ParanoidModel extends BaseModel {
|
||||
deletedAt: string | undefined;
|
||||
}
|
||||
@@ -10,9 +10,6 @@ class Pin extends BaseModel {
|
||||
@observable
|
||||
@Field
|
||||
index: string;
|
||||
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default Pin;
|
||||
|
||||
@@ -29,10 +29,6 @@ class Share extends BaseModel {
|
||||
url: string;
|
||||
|
||||
createdBy: User;
|
||||
|
||||
createdAt: string;
|
||||
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default Share;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { computed, observable } from "mobx";
|
||||
import { Role } from "@shared/types";
|
||||
import BaseModel from "./BaseModel";
|
||||
import ParanoidModel from "./ParanoidModel";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
class User extends BaseModel {
|
||||
class User extends ParanoidModel {
|
||||
@Field
|
||||
@observable
|
||||
id: string;
|
||||
@@ -34,8 +34,6 @@ class User extends BaseModel {
|
||||
|
||||
isSuspended: boolean;
|
||||
|
||||
createdAt: string;
|
||||
|
||||
@computed
|
||||
get isInvited(): boolean {
|
||||
return !this.lastActiveAt;
|
||||
|
||||
@@ -117,7 +117,7 @@ class AddGroupsToCollection extends React.Component<Props> {
|
||||
}
|
||||
items={groups.notInCollection(collection.id, this.query)}
|
||||
fetch={this.query ? undefined : groups.fetchPage}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: Group) => (
|
||||
<GroupListItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
|
||||
@@ -92,7 +92,6 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
</ButtonLink>
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder={`${t("Search by name")}…`}
|
||||
@@ -113,7 +112,7 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
}
|
||||
items={users.notInCollection(collection.id, this.query)}
|
||||
fetch={this.query ? undefined : users.fetchPage}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: User) => (
|
||||
<MemberListItem
|
||||
key={item.id}
|
||||
user={item}
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Group from "~/models/Group";
|
||||
import User from "~/models/User";
|
||||
import Button from "~/components/Button";
|
||||
import Divider from "~/components/Divider";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -292,7 +294,7 @@ function CollectionPermissions({ collection }: Props) {
|
||||
items={collectionGroups}
|
||||
fetch={collectionGroupMemberships.fetchPage}
|
||||
options={fetchOptions}
|
||||
renderItem={(group) => (
|
||||
renderItem={(group: Group) => (
|
||||
<CollectionGroupMemberListItem
|
||||
key={group.id}
|
||||
group={group}
|
||||
@@ -310,7 +312,7 @@ function CollectionPermissions({ collection }: Props) {
|
||||
items={collectionUsers}
|
||||
fetch={memberships.fetchPage}
|
||||
options={fetchOptions}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: User) => (
|
||||
<MemberListItem
|
||||
key={item.id}
|
||||
user={item}
|
||||
|
||||
@@ -113,7 +113,7 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||
}
|
||||
items={users.notInGroup(group.id, this.query)}
|
||||
fetch={this.query ? undefined : users.fetchPage}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: User) => (
|
||||
<GroupMemberListItem
|
||||
key={item.id}
|
||||
user={item}
|
||||
|
||||
@@ -103,7 +103,7 @@ function GroupMembers({ group }: Props) {
|
||||
id: group.id,
|
||||
}}
|
||||
empty={<Empty>{t("This group has no members.")}</Empty>}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: User) => (
|
||||
<GroupMemberListItem
|
||||
key={item.id}
|
||||
user={item}
|
||||
|
||||
@@ -89,7 +89,7 @@ function Export() {
|
||||
<Trans>Recent exports</Trans>
|
||||
</h2>
|
||||
}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: FileOperation) => (
|
||||
<FileOperationListItem
|
||||
key={item.id}
|
||||
fileOperation={item}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { PlusIcon, GroupIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import Group from "~/models/Group";
|
||||
import GroupNew from "~/scenes/GroupNew";
|
||||
import { Action } from "~/components/Actions";
|
||||
import Button from "~/components/Button";
|
||||
@@ -61,7 +62,7 @@ function Groups() {
|
||||
items={groups.orderedData}
|
||||
empty={<Empty>{t("No groups have been created yet")}</Empty>}
|
||||
fetch={groups.fetchPage}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: Group) => (
|
||||
<GroupListItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation, Trans } from "react-i18next";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import getDataTransferFiles from "@shared/utils/getDataTransferFiles";
|
||||
import { cdnPath } from "@shared/utils/urls";
|
||||
import FileOperation from "~/models/FileOperation";
|
||||
import Button from "~/components/Button";
|
||||
import Heading from "~/components/Heading";
|
||||
import Item from "~/components/List/Item";
|
||||
@@ -142,7 +143,7 @@ function Import() {
|
||||
<Trans>Recent imports</Trans>
|
||||
</h2>
|
||||
}
|
||||
renderItem={(item) => (
|
||||
renderItem={(item: FileOperation) => (
|
||||
<FileOperationListItem key={item.id} fileOperation={item} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { CodeIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import ApiKey from "~/models/ApiKey";
|
||||
import APITokenNew from "~/scenes/APITokenNew";
|
||||
import { Action } from "~/components/Actions";
|
||||
import Button from "~/components/Button";
|
||||
@@ -59,7 +60,7 @@ function Tokens() {
|
||||
fetch={apiKeys.fetchPage}
|
||||
items={apiKeys.orderedData}
|
||||
heading={<Subheading sticky>{t("Tokens")}</Subheading>}
|
||||
renderItem={(token) => (
|
||||
renderItem={(token: ApiKey) => (
|
||||
<TokenListItem key={token.id} token={token} onDelete={token.delete} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -217,7 +217,7 @@ export default abstract class BaseStore<T extends BaseModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params: FetchPageParams | undefined): Promise<any> => {
|
||||
fetchPage = async (params: FetchPageParams | undefined): Promise<T[]> => {
|
||||
if (!this.actions.includes(RPCAction.List)) {
|
||||
throw new Error(`Cannot list ${this.modelName}`);
|
||||
}
|
||||
|
||||
@@ -16,18 +16,22 @@ export default class CollectionGroupMembershipsStore extends BaseStore<
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
|
||||
fetchPage = async (
|
||||
params: PaginationParams | undefined
|
||||
): Promise<CollectionGroupMembership[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/collections.group_memberships`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: CollectionGroupMembership[] = [];
|
||||
runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => {
|
||||
res.data.groups.forEach(this.rootStore.groups.add);
|
||||
res.data.collectionGroupMemberships.forEach(this.add);
|
||||
models = res.data.collectionGroupMemberships.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return res.data.groups;
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -408,6 +408,7 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: document.id,
|
||||
ranking: result.ranking,
|
||||
context: result.context,
|
||||
document,
|
||||
|
||||
@@ -15,18 +15,22 @@ export default class GroupMembershipsStore extends BaseStore<GroupMembership> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
|
||||
fetchPage = async (
|
||||
params: PaginationParams | undefined
|
||||
): Promise<GroupMembership[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/groups.memberships`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: GroupMembership[] = [];
|
||||
runInAction(`GroupMembershipsStore#fetchPage`, () => {
|
||||
res.data.users.forEach(this.rootStore.users.add);
|
||||
res.data.groupMemberships.forEach(this.add);
|
||||
models = res.data.groupMemberships.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return res.data.users;
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -21,19 +21,21 @@ export default class GroupsStore extends BaseStore<Group> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params: FetchPageParams | undefined): Promise<any> => {
|
||||
fetchPage = async (params: FetchPageParams | undefined): Promise<Group[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/groups.list`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: Group[] = [];
|
||||
runInAction(`GroupsStore#fetchPage`, () => {
|
||||
this.addPolicies(res.policies);
|
||||
res.data.groups.forEach(this.add);
|
||||
models = res.data.groups.map(this.add);
|
||||
res.data.groupMemberships.forEach(this.rootStore.groupMemberships.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return res.data.groups;
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
@@ -72,7 +74,7 @@ export default class GroupsStore extends BaseStore<Group> {
|
||||
}
|
||||
|
||||
function queriedGroups(groups: Group[], query: string) {
|
||||
return filter(groups, (group) =>
|
||||
return groups.filter((group) =>
|
||||
group.name.toLowerCase().match(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,18 +14,22 @@ export default class MembershipsStore extends BaseStore<Membership> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
|
||||
fetchPage = async (
|
||||
params: PaginationParams | undefined
|
||||
): Promise<Membership[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/collections.memberships`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
runInAction(`/collections.memberships`, () => {
|
||||
|
||||
let models: Membership[] = [];
|
||||
runInAction(`MembershipsStore#fetchPage`, () => {
|
||||
res.data.users.forEach(this.rootStore.users.add);
|
||||
res.data.memberships.forEach(this.add);
|
||||
models = res.data.memberships.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return res.data.users;
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,22 @@ export default class PinsStore extends BaseStore<Pin> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params?: FetchParams | undefined): Promise<void> => {
|
||||
fetchPage = async (params?: FetchParams | undefined): Promise<Pin[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/pins.list`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: Pin[] = [];
|
||||
runInAction(`PinsStore#fetchPage`, () => {
|
||||
res.data.documents.forEach(this.rootStore.documents.add);
|
||||
res.data.pins.forEach(this.add);
|
||||
models = res.data.pins.map(this.add);
|
||||
this.addPolicies(res.policies);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -48,17 +48,21 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (options: PaginationParams | undefined): Promise<any> => {
|
||||
fetchPage = async (
|
||||
options: PaginationParams | undefined
|
||||
): Promise<Revision[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post("/revisions.list", options);
|
||||
invariant(res?.data, "Document revisions not available");
|
||||
|
||||
let models: Revision[] = [];
|
||||
runInAction("RevisionsStore#fetchPage", () => {
|
||||
res.data.forEach(this.add);
|
||||
models = res.data.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return res.data;
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -12,18 +12,23 @@ export default class StarsStore extends BaseStore<Star> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetchPage = async (params?: PaginationParams | undefined): Promise<void> => {
|
||||
fetchPage = async (
|
||||
params?: PaginationParams | undefined
|
||||
): Promise<Star[]> => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(`/stars.list`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: Star[] = [];
|
||||
runInAction(`StarsStore#fetchPage`, () => {
|
||||
res.data.documents.forEach(this.rootStore.documents.add);
|
||||
res.data.stars.forEach(this.add);
|
||||
models = res.data.stars.map(this.add);
|
||||
this.addPolicies(res.policies);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return models;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ export type PaginationParams = {
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
id: string;
|
||||
ranking: number;
|
||||
context: string;
|
||||
document: Document;
|
||||
|
||||
@@ -4,6 +4,12 @@ import CollectionUser from "./CollectionUser";
|
||||
import UserAuthentication from "./UserAuthentication";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("user model", () => {
|
||||
describe("destroy", () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ Object {
|
||||
"isViewer": false,
|
||||
"lastActiveAt": undefined,
|
||||
"name": "Test User",
|
||||
"updatedAt": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -25,5 +26,6 @@ Object {
|
||||
"isViewer": false,
|
||||
"lastActiveAt": undefined,
|
||||
"name": "Test User",
|
||||
"updatedAt": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -6,5 +6,6 @@ export default function present(key: ApiKey) {
|
||||
name: key.name,
|
||||
secret: key.secret,
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ export default function present(data: FileOperation) {
|
||||
collectionId: data.collectionId,
|
||||
user: presentUser(data.user),
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type UserPresentation = {
|
||||
name: string;
|
||||
avatarUrl: string | null | undefined;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
lastActiveAt: Date | null;
|
||||
color: string;
|
||||
isAdmin: boolean;
|
||||
@@ -31,6 +32,7 @@ export default (
|
||||
isSuspended: user.isSuspended,
|
||||
isViewer: user.isViewer,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
lastActiveAt: user.lastActiveAt,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
@@ -68,6 +69,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
@@ -104,6 +106,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
@@ -140,6 +143,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
@@ -194,6 +198,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
@@ -257,6 +262,7 @@ Object {
|
||||
"language": "en_US",
|
||||
"lastActiveAt": null,
|
||||
"name": "User 1",
|
||||
"updatedAt": "2018-01-02T00:00:00.000Z",
|
||||
},
|
||||
"ok": true,
|
||||
"policies": Array [
|
||||
|
||||
@@ -6,7 +6,14 @@ import { flushdb, seed } from "@server/test/support";
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
beforeEach(() => flushdb());
|
||||
afterAll(() => server.close());
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date("2018-01-02T00:00:00.000Z"));
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
return server.close();
|
||||
});
|
||||
|
||||
describe("#users.list", () => {
|
||||
it("should allow filtering by user name", async () => {
|
||||
|
||||
@@ -135,7 +135,8 @@ export async function buildUser(overrides: Partial<User> = {}) {
|
||||
name: `User ${count}`,
|
||||
username: `user${count}`,
|
||||
createdAt: new Date("2018-01-01T00:00:00.000Z"),
|
||||
lastActiveAt: new Date("2018-01-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2018-01-02T00:00:00.000Z"),
|
||||
lastActiveAt: new Date("2018-01-03T00:00:00.000Z"),
|
||||
authentications: [
|
||||
{
|
||||
authenticationProviderId: authenticationProvider!.id,
|
||||
|
||||
Reference in New Issue
Block a user