Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Models are not all removed from local store upon access change (#…
…3729) * fix: Clean data from stores correctly on 401/403 response * Convert DataLoader from class component, remove observables and caching * types
- Loading branch information
Showing
7 changed files
with
167 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,234 +1,164 @@ | ||
import invariant from "invariant"; | ||
import { observable } from "mobx"; | ||
import { observer } from "mobx-react"; | ||
import * as React from "react"; | ||
import { RouteComponentProps, StaticContext } from "react-router"; | ||
import RootStore from "~/stores/RootStore"; | ||
import { useLocation, RouteComponentProps, StaticContext } from "react-router"; | ||
import Document from "~/models/Document"; | ||
import Revision from "~/models/Revision"; | ||
import Error404 from "~/scenes/Error404"; | ||
import ErrorOffline from "~/scenes/ErrorOffline"; | ||
import withStores from "~/components/withStores"; | ||
import usePolicy from "~/hooks/usePolicy"; | ||
import useStores from "~/hooks/useStores"; | ||
import { NavigationNode } from "~/types"; | ||
import { NotFoundError, OfflineError } from "~/utils/errors"; | ||
import history from "~/utils/history"; | ||
import { matchDocumentEdit } from "~/utils/routeHelpers"; | ||
import HideSidebar from "./HideSidebar"; | ||
import Loading from "./Loading"; | ||
|
||
type Props = RootStore & | ||
RouteComponentProps< | ||
{ | ||
documentSlug: string; | ||
revisionId?: string; | ||
shareId?: string; | ||
title?: string; | ||
}, | ||
StaticContext, | ||
{ | ||
title?: string; | ||
} | ||
> & { | ||
children: (arg0: any) => React.ReactNode; | ||
}; | ||
|
||
@observer | ||
class DataLoader extends React.Component<Props> { | ||
sharedTree: NavigationNode | null | undefined; | ||
|
||
@observable | ||
document: Document | null | undefined; | ||
|
||
@observable | ||
revision: Revision | null | undefined; | ||
|
||
@observable | ||
shapshot: Blob | null | undefined; | ||
|
||
@observable | ||
error: Error | null | undefined; | ||
|
||
componentDidMount() { | ||
const { documents, match } = this.props; | ||
this.document = documents.getByUrl(match.params.documentSlug); | ||
this.sharedTree = this.document | ||
? documents.getSharedTree(this.document.id) | ||
: undefined; | ||
this.loadDocument(); | ||
} | ||
|
||
componentDidUpdate(prevProps: Props) { | ||
// If we have the document in the store, but not it's policy then we need to | ||
// reload from the server otherwise the UI will not know which authorizations | ||
// the user has | ||
if (this.document) { | ||
const document = this.document; | ||
const policy = this.props.policies.get(document.id); | ||
|
||
if ( | ||
!policy && | ||
!this.error && | ||
this.props.auth.user && | ||
this.props.auth.user.id | ||
) { | ||
this.loadDocument(); | ||
type Params = { | ||
documentSlug: string; | ||
revisionId?: string; | ||
shareId?: string; | ||
}; | ||
|
||
type LocationState = { | ||
title?: string; | ||
restore?: boolean; | ||
revisionId?: string; | ||
}; | ||
|
||
type Children = (options: { | ||
document: Document; | ||
revision: Revision | undefined; | ||
abilities: Record<string, boolean>; | ||
isEditing: boolean; | ||
readOnly: boolean; | ||
onCreateLink: (title: string) => Promise<string>; | ||
sharedTree: NavigationNode | undefined; | ||
}) => React.ReactNode; | ||
|
||
type Props = RouteComponentProps<Params, StaticContext, LocationState> & { | ||
children: Children; | ||
}; | ||
|
||
function DataLoader({ match, children }: Props) { | ||
const { ui, shares, documents, auth, revisions } = useStores(); | ||
const { team } = auth; | ||
const [error, setError] = React.useState<Error | null>(null); | ||
const { revisionId, shareId, documentSlug } = match.params; | ||
const document = documents.getByUrl(match.params.documentSlug); | ||
const revision = revisionId ? revisions.get(revisionId) : undefined; | ||
const sharedTree = document | ||
? documents.getSharedTree(document.id) | ||
: undefined; | ||
const isEditRoute = match.path === matchDocumentEdit; | ||
const isEditing = isEditRoute || !!auth.team?.collaborativeEditing; | ||
const can = usePolicy(document ? document.id : ""); | ||
const location = useLocation<LocationState>(); | ||
|
||
React.useEffect(() => { | ||
async function fetchDocument() { | ||
try { | ||
await documents.fetchWithSharedTree(documentSlug, { | ||
shareId, | ||
}); | ||
} catch (err) { | ||
setError(err); | ||
} | ||
} | ||
fetchDocument(); | ||
}, [ui, documents, document, shareId, documentSlug]); | ||
|
||
// Also need to load the revision if it changes | ||
const { revisionId } = this.props.match.params; | ||
|
||
if ( | ||
prevProps.match.params.revisionId !== revisionId && | ||
revisionId && | ||
revisionId !== "latest" | ||
) { | ||
this.loadRevision(); | ||
} | ||
} | ||
|
||
get isEditRoute() { | ||
return this.props.match.path === matchDocumentEdit; | ||
} | ||
|
||
get isEditing() { | ||
return this.isEditRoute || this.props.auth?.team?.collaborativeEditing; | ||
} | ||
|
||
onCreateLink = async (title: string) => { | ||
const document = this.document; | ||
invariant(document, "document must be loaded to create link"); | ||
|
||
const newDocument = await this.props.documents.create({ | ||
collectionId: document.collectionId, | ||
parentDocumentId: document.parentDocumentId, | ||
title, | ||
text: "", | ||
}); | ||
|
||
return newDocument.url; | ||
}; | ||
|
||
loadRevision = async () => { | ||
const { revisionId } = this.props.match.params; | ||
|
||
if (revisionId) { | ||
this.revision = await this.props.revisions.fetch(revisionId); | ||
} | ||
}; | ||
|
||
loadDocument = async () => { | ||
const { shareId, documentSlug, revisionId } = this.props.match.params; | ||
|
||
// sets the document as active in the sidebar if we already have it loaded | ||
if (this.document) { | ||
this.props.ui.setActiveDocument(this.document); | ||
} | ||
|
||
try { | ||
const response = await this.props.documents.fetchWithSharedTree( | ||
documentSlug, | ||
{ | ||
shareId, | ||
} | ||
); | ||
this.sharedTree = response.sharedTree; | ||
this.document = response.document; | ||
|
||
React.useEffect(() => { | ||
async function fetchRevision() { | ||
if (revisionId && revisionId !== "latest") { | ||
await this.loadRevision(); | ||
} else { | ||
this.revision = undefined; | ||
try { | ||
await revisions.fetch(revisionId); | ||
} catch (err) { | ||
setError(err); | ||
} | ||
} | ||
} catch (err) { | ||
this.error = err; | ||
return; | ||
} | ||
fetchRevision(); | ||
}, [revisions, revisionId]); | ||
|
||
const onCreateLink = React.useCallback( | ||
async (title: string) => { | ||
if (!document) { | ||
throw new Error("Document not loaded yet"); | ||
} | ||
|
||
const document = this.document; | ||
const newDocument = await documents.create({ | ||
collectionId: document.collectionId, | ||
parentDocumentId: document.parentDocumentId, | ||
title, | ||
text: "", | ||
}); | ||
|
||
return newDocument.url; | ||
}, | ||
[document, documents] | ||
); | ||
|
||
React.useEffect(() => { | ||
if (document) { | ||
const can = this.props.policies.abilities(document.id); | ||
// sets the document as active in the sidebar, ideally in the future this | ||
// will be route driven. | ||
this.props.ui.setActiveDocument(document); | ||
// sets the current document as active in the sidebar | ||
ui.setActiveDocument(document); | ||
|
||
// If we're attempting to update an archived, deleted, or otherwise | ||
// uneditable document then forward to the canonical read url. | ||
if (!can.update && this.isEditRoute) { | ||
if (!can.update && isEditRoute) { | ||
history.push(document.url); | ||
return; | ||
} | ||
|
||
// Prevents unauthorized request to load share information for the document | ||
// when viewing a public share link | ||
if (can.read) { | ||
this.props.shares.fetch(document.id).catch((err) => { | ||
shares.fetch(document.id).catch((err) => { | ||
if (!(err instanceof NotFoundError)) { | ||
throw err; | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
render() { | ||
const { location, policies, auth, match, ui } = this.props; | ||
const { revisionId } = match.params; | ||
|
||
if (this.error) { | ||
return this.error instanceof OfflineError ? ( | ||
<ErrorOffline /> | ||
) : ( | ||
<Error404 /> | ||
); | ||
} | ||
|
||
const team = auth.team; | ||
const document = this.document; | ||
const revision = this.revision; | ||
|
||
if (!document || !team || (revisionId && !revision)) { | ||
return ( | ||
<> | ||
<Loading location={location} /> | ||
{this.isEditing && !team?.collaborativeEditing && ( | ||
<HideSidebar ui={ui} /> | ||
)} | ||
</> | ||
); | ||
} | ||
}, [can.read, can.update, document, isEditRoute, shares, ui]); | ||
|
||
const abilities = policies.abilities(document.id); | ||
// We do not want to remount the document when changing from view->edit | ||
// on the multiplayer flag as the doc is guaranteed to be upto date. | ||
const key = team.collaborativeEditing | ||
? "" | ||
: this.isEditing | ||
? "editing" | ||
: "read-only"; | ||
if (error) { | ||
return error instanceof OfflineError ? <ErrorOffline /> : <Error404 />; | ||
} | ||
|
||
if (!document || !team || (revisionId && !revision)) { | ||
return ( | ||
<React.Fragment key={key}> | ||
{this.isEditing && !team.collaborativeEditing && ( | ||
<HideSidebar ui={ui} /> | ||
)} | ||
{this.props.children({ | ||
document, | ||
revision, | ||
abilities, | ||
isEditing: this.isEditing, | ||
readOnly: | ||
!this.isEditing || | ||
!abilities.update || | ||
document.isArchived || | ||
!!revisionId, | ||
onCreateLink: this.onCreateLink, | ||
sharedTree: this.sharedTree, | ||
})} | ||
</React.Fragment> | ||
<> | ||
<Loading location={location} /> | ||
{isEditing && !team?.collaborativeEditing && <HideSidebar ui={ui} />} | ||
</> | ||
); | ||
} | ||
|
||
// We do not want to remount the document when changing from view->edit | ||
// on the multiplayer flag as the doc is guaranteed to be upto date. | ||
const key = team.collaborativeEditing | ||
? "" | ||
: isEditing | ||
? "editing" | ||
: "read-only"; | ||
|
||
return ( | ||
<React.Fragment key={key}> | ||
{isEditing && !team.collaborativeEditing && <HideSidebar ui={ui} />} | ||
{children({ | ||
document, | ||
revision, | ||
abilities: can, | ||
isEditing, | ||
readOnly: | ||
!isEditing || !can.update || document.isArchived || !!revisionId, | ||
onCreateLink, | ||
sharedTree, | ||
})} | ||
</React.Fragment> | ||
); | ||
} | ||
|
||
export default withStores(DataLoader); | ||
export default observer(DataLoader); |
Oops, something went wrong.