feat: Memberships (#1032)
* WIP * feat: Add collection.memberships endpoint * feat: Add ability to filter collection.memberships with query * WIP * Merge stashed work * feat: Add ability to filter memberships by permission * continued refactoring * paginated list component * Collection member management * fix: Incorrect policy data sent down after collection.update * Reduce duplication, add empty state * cleanup * fix: Modal close should be a real button * fix: Allow opening edit from modal * fix: remove unused methods * test: fix * Passing test suite * Refactor * fix: Flow UI errors * test: Add collections.update tests * lint * test: moar tests * fix: Missing scopes, more missing tests * fix: Handle collection privacy change over socket * fix: More membership scopes * fix: view endpoint permissions * fix: respond to privacy change on socket event * policy driven menus * fix: share endpoint policies * chore: Use policies to drive documents UI * alignment * fix: Header height * fix: Correct behavior when collection becomes private * fix: Header height for read-only collection * send id's over socket instead of serialized objects * fix: Remote policy change * fix: reduce collection fetching * More websocket efficiencies * fix: Document collection pinning * fix: Restored ability to edit drafts fix: Removed ability to star drafts * fix: Require write permissions to pin doc to collection * fix: Header title overlaying document actions at small screen sizes * fix: Jank on load caused by previous commit * fix: Double collection fetch post-publish * fix: Hide publish button if draft is in no longer accessible collection * fix: Always allow deleting drafts fix: Improved handling of deleted documents * feat: Show collections in drafts view feat: Show more obvious 'draft' badge on documents * fix: incorrect policies after publish to private collection * fix: Duplicating a draft publishes it
This commit is contained in:
@@ -7,6 +7,7 @@ export const Action = styled(Flex)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 0 0 12px;
|
||||
height: 32px;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ const Badge = styled.span`
|
||||
margin-left: 10px;
|
||||
padding: 2px 6px 3px;
|
||||
background-color: ${({ admin, theme }) =>
|
||||
admin ? theme.primary : theme.smokeDark};
|
||||
color: ${({ admin, theme }) => (admin ? theme.white : theme.text)};
|
||||
border-radius: 2px;
|
||||
admin ? theme.primary : theme.textTertiary};
|
||||
color: ${({ admin, theme }) => (admin ? theme.white : theme.background)};
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Link } from 'react-router-dom';
|
||||
import { StarredIcon } from 'outline-icons';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Badge from 'components/Badge';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Highlight from 'components/Highlight';
|
||||
import PublishingInfo from 'components/PublishingInfo';
|
||||
import DocumentMenu from 'menus/DocumentMenu';
|
||||
@@ -17,6 +19,7 @@ type Props = {
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
showPin?: boolean,
|
||||
showDraft?: boolean,
|
||||
};
|
||||
|
||||
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
|
||||
@@ -130,6 +133,7 @@ class DocumentPreview extends React.Component<Props> {
|
||||
showCollection,
|
||||
showPublished,
|
||||
showPin,
|
||||
showDraft = true,
|
||||
highlight,
|
||||
context,
|
||||
...rest
|
||||
@@ -159,6 +163,16 @@ class DocumentPreview extends React.Component<Props> {
|
||||
)}
|
||||
</Actions>
|
||||
)}
|
||||
{document.isDraft &&
|
||||
showDraft && (
|
||||
<Tooltip
|
||||
tooltip="Only visible to you"
|
||||
delay={500}
|
||||
placement="top"
|
||||
>
|
||||
<Badge>Draft</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<StyledDocumentMenu document={document} showPin={showPin} />
|
||||
</Heading>
|
||||
{!queryIsInTitle && (
|
||||
|
||||
@@ -4,9 +4,11 @@ import invariant from 'invariant';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { PortalWithState } from 'react-portal';
|
||||
import { MoreIcon } from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import NudeButton from 'components/NudeButton';
|
||||
|
||||
let previousClosePortal;
|
||||
|
||||
@@ -15,7 +17,7 @@ type Children =
|
||||
| ((options: { closePortal: () => void }) => React.Node);
|
||||
|
||||
type Props = {
|
||||
label: React.Node,
|
||||
label?: React.Node,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
children?: Children,
|
||||
@@ -76,7 +78,11 @@ class DropdownMenu extends React.Component<Props> {
|
||||
{({ closePortal, openPortal, portal }) => (
|
||||
<React.Fragment>
|
||||
<Label onClick={this.handleOpen(openPortal, closePortal)}>
|
||||
{label}
|
||||
{label || (
|
||||
<NudeButton>
|
||||
<MoreIcon />
|
||||
</NudeButton>
|
||||
)}
|
||||
</Label>
|
||||
{portal(
|
||||
<Position
|
||||
|
||||
@@ -8,9 +8,13 @@ type Props = {
|
||||
disabled?: boolean,
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({ onClick, children, ...rest }: Props) => {
|
||||
const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
|
||||
return (
|
||||
<MenuItem onClick={onClick} {...rest}>
|
||||
<MenuItem
|
||||
onClick={disabled ? undefined : onClick}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</MenuItem>
|
||||
);
|
||||
@@ -33,9 +37,13 @@ const MenuItem = styled.a`
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: ${props => (props.disabled ? '.5' : 1)};
|
||||
}
|
||||
|
||||
${props =>
|
||||
props.disabled
|
||||
? ''
|
||||
? 'pointer-events: none;'
|
||||
: `
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Input, { LabelText, Outline } from 'components/Input';
|
||||
import { LabelText, Outline } from 'components/Input';
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
@@ -41,26 +41,22 @@ class InputRich extends React.Component<Props> {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<LabelText>{label}</LabelText>
|
||||
{Editor ? (
|
||||
<StyledOutline
|
||||
maxHeight={maxHeight}
|
||||
minHeight={minHeight}
|
||||
focused={this.focused}
|
||||
>
|
||||
|
||||
<StyledOutline
|
||||
maxHeight={maxHeight}
|
||||
minHeight={minHeight}
|
||||
focused={this.focused}
|
||||
>
|
||||
{Editor ? (
|
||||
<Editor
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
{...rest}
|
||||
/>
|
||||
</StyledOutline>
|
||||
) : (
|
||||
<Input
|
||||
maxHeight={maxHeight}
|
||||
minHeight={minHeight}
|
||||
placeholder="Loading…"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
'Loading…'
|
||||
)}
|
||||
</StyledOutline>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
72
app/components/InputSelect.js
Normal file
72
app/components/InputSelect.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import styled from 'styled-components';
|
||||
import VisuallyHidden from 'components/VisuallyHidden';
|
||||
import { Outline, LabelText } from './Input';
|
||||
|
||||
const Select = styled.select`
|
||||
border: 0;
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
background: none;
|
||||
color: ${props => props.theme.text};
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${props => props.theme.placeholder};
|
||||
}
|
||||
`;
|
||||
|
||||
type Option = { label: string, value: string };
|
||||
|
||||
export type Props = {
|
||||
value?: string,
|
||||
label?: string,
|
||||
className?: string,
|
||||
labelHidden?: boolean,
|
||||
options: Option[],
|
||||
};
|
||||
|
||||
@observer
|
||||
class InputSelect extends React.Component<Props> {
|
||||
@observable focused: boolean = false;
|
||||
|
||||
handleBlur = () => {
|
||||
this.focused = false;
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.focused = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, className, labelHidden, options, ...rest } = this.props;
|
||||
|
||||
const wrappedLabel = <LabelText>{label}</LabelText>;
|
||||
|
||||
return (
|
||||
<label>
|
||||
{label &&
|
||||
(labelHidden ? (
|
||||
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
|
||||
) : (
|
||||
wrappedLabel
|
||||
))}
|
||||
<Outline focused={this.focused} className={className}>
|
||||
<Select onBlur={this.handleBlur} onFocus={this.handleFocus} {...rest}>
|
||||
{options.map(option => (
|
||||
<option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Outline>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InputSelect;
|
||||
@@ -30,6 +30,10 @@ const Wrapper = styled.li`
|
||||
padding: ${props => (props.compact ? '8px' : '12px')} 0;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid ${props => props.theme.divider};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Image = styled(Flex)`
|
||||
|
||||
@@ -6,6 +6,7 @@ import breakpoint from 'styled-components-breakpoint';
|
||||
import ReactModal from 'react-modal';
|
||||
import { transparentize } from 'polished';
|
||||
import { CloseIcon } from 'outline-icons';
|
||||
import NudeButton from 'components/NudeButton';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
@@ -90,16 +91,18 @@ const StyledModal = styled(ReactModal)`
|
||||
const Esc = styled.span`
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: -10px;
|
||||
font-size: 13px;
|
||||
height: 1em;
|
||||
`;
|
||||
|
||||
const Close = styled.a`
|
||||
const Close = styled(NudeButton)`
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
opacity: 0.75;
|
||||
color: ${props => props.theme.text};
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
95
app/components/PaginatedList.js
Normal file
95
app/components/PaginatedList.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
|
||||
type Props = {
|
||||
fetch?: (options: ?Object) => Promise<void>,
|
||||
options?: Object,
|
||||
empty?: React.Node,
|
||||
items: any[],
|
||||
renderItem: any => React.Node,
|
||||
};
|
||||
|
||||
@observer
|
||||
class PaginatedList extends React.Component<Props> {
|
||||
isInitiallyLoaded: boolean = false;
|
||||
@observable isLoaded: boolean = false;
|
||||
@observable isFetchingMore: boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@observable offset: number = 0;
|
||||
@observable allowLoadMore: boolean = true;
|
||||
|
||||
componentDidMount() {
|
||||
this.isInitiallyLoaded = !!this.props.items.length;
|
||||
this.fetchResults();
|
||||
}
|
||||
|
||||
fetchResults = async () => {
|
||||
if (!this.props.fetch) return;
|
||||
|
||||
this.isFetching = true;
|
||||
|
||||
const limit = DEFAULT_PAGINATION_LIMIT;
|
||||
const results = await this.props.fetch({
|
||||
limit,
|
||||
offset: this.offset,
|
||||
...this.props.options,
|
||||
});
|
||||
|
||||
if (results && (results.length === 0 || results.length < limit)) {
|
||||
this.allowLoadMore = false;
|
||||
} else {
|
||||
this.offset += limit;
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
this.isFetching = false;
|
||||
this.isFetchingMore = false;
|
||||
};
|
||||
|
||||
@action
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||
if (!this.allowLoadMore || this.isFetching) return;
|
||||
|
||||
this.isFetchingMore = true;
|
||||
await this.fetchResults();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items, empty } = this.props;
|
||||
|
||||
const showLoading =
|
||||
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
|
||||
const showEmpty = !items.length || showLoading;
|
||||
const showList = (this.isLoaded || this.isInitiallyLoaded) && !showLoading;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showEmpty && empty}
|
||||
{showList && (
|
||||
<React.Fragment>
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{items.map(this.props.renderItem)}
|
||||
</ArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{showLoading && <ListPlaceholder count={5} />}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PaginatedList;
|
||||
@@ -91,7 +91,7 @@ function PublishingInfo({
|
||||
<span>
|
||||
in
|
||||
<strong>
|
||||
{isDraft ? 'Drafts' : <Breadcrumb document={document} onlyText />}
|
||||
<Breadcrumb document={document} onlyText />
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,7 @@ class MainSidebar extends React.Component<Props> {
|
||||
if (!user || !team) return null;
|
||||
|
||||
const draftDocumentsCount = documents.drafts.length;
|
||||
const can = policies.abilties(team.id);
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
|
||||
@@ -43,7 +43,7 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
const { team } = auth;
|
||||
if (!team) return null;
|
||||
|
||||
const can = policies.abilties(team.id);
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
|
||||
@@ -13,11 +13,13 @@ import CollectionLink from './CollectionLink';
|
||||
import Fade from 'components/Fade';
|
||||
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
policies: PoliciesStore,
|
||||
collections: CollectionsStore,
|
||||
documents: DocumentsStore,
|
||||
onCreateCollection: () => void,
|
||||
@@ -41,6 +43,9 @@ class Collections extends React.Component<Props> {
|
||||
const { activeCollectionId } = this.props.ui;
|
||||
if (!activeCollectionId) return;
|
||||
|
||||
const can = this.props.policies.abilities(activeCollectionId);
|
||||
if (!can.update) return;
|
||||
|
||||
this.props.history.push(newDocumentUrl(activeCollectionId));
|
||||
}
|
||||
|
||||
@@ -75,6 +80,6 @@ class Collections extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('collections', 'ui', 'documents')(
|
||||
export default inject('collections', 'ui', 'documents', 'policies')(
|
||||
withRouter(Collections)
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { inject } from 'mobx-react';
|
||||
import { find } from 'lodash';
|
||||
import io from 'socket.io-client';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import MembershipsStore from 'stores/MembershipsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
@@ -13,6 +16,8 @@ type Props = {
|
||||
children: React.Node,
|
||||
documents: DocumentsStore,
|
||||
collections: CollectionsStore,
|
||||
memberships: MembershipsStore,
|
||||
policies: PoliciesStore,
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
@@ -27,34 +32,81 @@ class SocketProvider extends React.Component<Props> {
|
||||
path: '/realtime',
|
||||
});
|
||||
|
||||
const { auth, ui, documents, collections } = this.props;
|
||||
const {
|
||||
auth,
|
||||
ui,
|
||||
documents,
|
||||
collections,
|
||||
memberships,
|
||||
policies,
|
||||
} = this.props;
|
||||
if (!auth.token) return;
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.socket.emit('authentication', {
|
||||
token: auth.token,
|
||||
});
|
||||
|
||||
this.socket.on('unauthorized', err => {
|
||||
ui.showToast(err.message);
|
||||
throw err;
|
||||
});
|
||||
this.socket.on('entities', event => {
|
||||
if (event.documents) {
|
||||
event.documents.forEach(doc => {
|
||||
if (doc.deletedAt) {
|
||||
documents.remove(doc.id);
|
||||
} else {
|
||||
documents.add(doc);
|
||||
|
||||
this.socket.on('entities', async event => {
|
||||
if (event.documentIds) {
|
||||
for (const documentDescriptor of event.documentIds) {
|
||||
const documentId = documentDescriptor.id;
|
||||
let document = documents.get(documentId) || {};
|
||||
|
||||
if (event.event === 'documents.delete') {
|
||||
documents.remove(documentId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we already have the latest version (it was us that performed the change)
|
||||
// the we don't need to update anything either.
|
||||
const { title, updatedAt } = document;
|
||||
if (updatedAt === documentDescriptor.updatedAt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise, grab the latest version of the document
|
||||
try {
|
||||
document = await documents.fetch(documentId, {
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404 || err.statusCode === 403) {
|
||||
documents.remove(documentId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if the title changed then we need to update the collection also
|
||||
if (title !== document.title) {
|
||||
if (!event.collectionIds) {
|
||||
event.collectionIds = [];
|
||||
}
|
||||
|
||||
const existing = find(event.collectionIds, {
|
||||
id: document.collectionId,
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
event.collectionIds.push({
|
||||
id: document.collectionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this to the document scene once data loading
|
||||
// has been refactored to be friendlier there.
|
||||
if (
|
||||
auth.user &&
|
||||
doc.id === ui.activeDocumentId &&
|
||||
doc.updatedBy.id !== auth.user.id
|
||||
documentId === ui.activeDocumentId &&
|
||||
document.updatedBy.id !== auth.user.id
|
||||
) {
|
||||
ui.showToast(`Document updated by ${doc.updatedBy.name}`, {
|
||||
ui.showToast(`Document updated by ${document.updatedBy.name}`, {
|
||||
timeout: 30 * 1000,
|
||||
action: {
|
||||
text: 'Refresh',
|
||||
@@ -62,26 +114,69 @@ class SocketProvider extends React.Component<Props> {
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (event.collections) {
|
||||
event.collections.forEach(collection => {
|
||||
if (collection.deletedAt) {
|
||||
collections.remove(collection.id);
|
||||
documents.removeCollectionDocuments(collection.id);
|
||||
} else {
|
||||
collections.add(collection);
|
||||
|
||||
if (event.collectionIds) {
|
||||
for (const collectionDescriptor of event.collectionIds) {
|
||||
const collectionId = collectionDescriptor.id;
|
||||
const collection = collections.get(collectionId) || {};
|
||||
|
||||
if (event.event === 'collections.delete') {
|
||||
documents.remove(collectionId);
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
// if we already have the latest version (it was us that performed the change)
|
||||
// the we don't need to update anything either.
|
||||
const { updatedAt } = collection;
|
||||
if (updatedAt === collectionDescriptor.updatedAt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await collections.fetch(collectionId, { force: true });
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404 || err.statusCode === 403) {
|
||||
collections.remove(collectionId);
|
||||
documents.removeCollectionDocuments(collectionId);
|
||||
memberships.removeCollectionMemberships(collectionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('documents.star', event => {
|
||||
documents.starredIds.set(event.documentId, true);
|
||||
});
|
||||
|
||||
this.socket.on('documents.unstar', event => {
|
||||
documents.starredIds.set(event.documentId, false);
|
||||
});
|
||||
|
||||
this.socket.on('collections.add_user', event => {
|
||||
if (auth.user && event.userId === auth.user.id) {
|
||||
collections.fetch(event.collectionId, { force: true });
|
||||
}
|
||||
|
||||
// Document policies might need updating as the permission changes
|
||||
documents.inCollection(event.collectionId).forEach(document => {
|
||||
policies.remove(document.id);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('collections.remove_user', event => {
|
||||
if (auth.user && event.userId === auth.user.id) {
|
||||
collections.remove(event.collectionId);
|
||||
memberships.removeCollectionMemberships(event.collectionId);
|
||||
documents.removeCollectionDocuments(event.collectionId);
|
||||
} else {
|
||||
memberships.remove(`${event.userId}-${event.collectionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// received a message from the API server that we should request
|
||||
// to join a specific room. Forward that to the ws server.
|
||||
this.socket.on('join', event => {
|
||||
@@ -96,6 +191,10 @@ class SocketProvider extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SocketContext.Provider value={this.socket}>
|
||||
@@ -105,4 +204,11 @@ class SocketProvider extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('auth', 'ui', 'documents', 'collections')(SocketProvider);
|
||||
export default inject(
|
||||
'auth',
|
||||
'ui',
|
||||
'documents',
|
||||
'collections',
|
||||
'memberships',
|
||||
'policies'
|
||||
)(SocketProvider);
|
||||
|
||||
Reference in New Issue
Block a user