feat: Sharing improvements (#1388)
* add migrations * first pass at API * feat: Updated share dialog UI * tests * test * styling tweaks * feat: Show share state on document * fix: Allow publishing share links for draft docs * test: shares.info
This commit is contained in:
parent
0b33b5bc05
commit
169ad5b025
@ -95,13 +95,13 @@ const StyledEditor = styled(RichMarkdownEditor)`
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
a {
|
a {
|
||||||
color: ${props => props.theme.link};
|
color: ${props => props.theme.text};
|
||||||
border-bottom: 1px solid ${props => lighten(0.5, props.theme.link)};
|
border-bottom: 1px solid ${props => lighten(0.5, props.theme.text)};
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-bottom: 1px solid ${props => props.theme.link};
|
border-bottom: 1px solid ${props => props.theme.text};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ export const Outline = styled(Flex)`
|
|||||||
export const LabelText = styled.div`
|
export const LabelText = styled.div`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
|
display: inline-block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
@ -22,7 +22,7 @@ function Switch({ width = 38, height = 20, label, ...props }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Label htmlFor={props.id}>
|
<Label htmlFor={props.id}>
|
||||||
{component}
|
{component}
|
||||||
<LabelText> {label}</LabelText>
|
<LabelText>{label}</LabelText>
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -41,6 +41,7 @@ const Wrapper = styled.label`
|
|||||||
width: ${props => props.width}px;
|
width: ${props => props.width}px;
|
||||||
height: ${props => props.height}px;
|
height: ${props => props.height}px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Slider = styled.span`
|
const Slider = styled.span`
|
||||||
|
@ -18,6 +18,10 @@ class Tooltip extends React.Component<Props> {
|
|||||||
|
|
||||||
let content = tooltip;
|
let content = tooltip;
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
|
||||||
if (shortcut) {
|
if (shortcut) {
|
||||||
content = (
|
content = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -110,8 +110,7 @@ class DocumentMenu extends React.Component<Props> {
|
|||||||
|
|
||||||
handleShareLink = async (ev: SyntheticEvent<>) => {
|
handleShareLink = async (ev: SyntheticEvent<>) => {
|
||||||
const { document } = this.props;
|
const { document } = this.props;
|
||||||
if (!document.shareUrl) await document.share();
|
await document.share();
|
||||||
|
|
||||||
this.props.ui.setActiveModal("document-share", { document });
|
this.props.ui.setActiveModal("document-share", { document });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ export default class Document extends BaseModel {
|
|||||||
deletedAt: ?string;
|
deletedAt: ?string;
|
||||||
url: string;
|
url: string;
|
||||||
urlId: string;
|
urlId: string;
|
||||||
shareUrl: ?string;
|
|
||||||
revision: number;
|
revision: number;
|
||||||
|
|
||||||
get emoji() {
|
get emoji() {
|
||||||
@ -90,10 +89,7 @@ export default class Document extends BaseModel {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
share = async () => {
|
share = async () => {
|
||||||
const res = await client.post("/shares.create", { documentId: this.id });
|
return this.store.rootStore.shares.create({ documentId: this.id });
|
||||||
invariant(res && res.data, "Share data should be available");
|
|
||||||
this.shareUrl = res.data.url;
|
|
||||||
return this.shareUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -17,6 +17,7 @@ class Event extends BaseModel {
|
|||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
published: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
get model() {
|
get model() {
|
||||||
|
@ -5,6 +5,8 @@ import User from "./User";
|
|||||||
class Share extends BaseModel {
|
class Share extends BaseModel {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
published: boolean;
|
||||||
|
documentId: string;
|
||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
documentUrl: string;
|
documentUrl: string;
|
||||||
createdBy: User;
|
createdBy: User;
|
||||||
|
@ -15,6 +15,7 @@ import HideSidebar from "./HideSidebar";
|
|||||||
import Error404 from "scenes/Error404";
|
import Error404 from "scenes/Error404";
|
||||||
import ErrorOffline from "scenes/ErrorOffline";
|
import ErrorOffline from "scenes/ErrorOffline";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
|
import SharesStore from "stores/SharesStore";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
import RevisionsStore from "stores/RevisionsStore";
|
import RevisionsStore from "stores/RevisionsStore";
|
||||||
import UiStore from "stores/UiStore";
|
import UiStore from "stores/UiStore";
|
||||||
@ -23,6 +24,7 @@ import { OfflineError } from "utils/errors";
|
|||||||
type Props = {|
|
type Props = {|
|
||||||
match: Object,
|
match: Object,
|
||||||
location: Location,
|
location: Location,
|
||||||
|
shares: SharesStore,
|
||||||
documents: DocumentsStore,
|
documents: DocumentsStore,
|
||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
revisions: RevisionsStore,
|
revisions: RevisionsStore,
|
||||||
@ -128,6 +130,8 @@ class DataLoader extends React.Component<Props> {
|
|||||||
return this.goToDocumentCanonical();
|
return this.goToDocumentCanonical();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.shares.fetch(document.id);
|
||||||
|
|
||||||
const isMove = this.props.location.pathname.match(/move$/);
|
const isMove = this.props.location.pathname.match(/move$/);
|
||||||
const canRedirect = !revisionId && !isMove && !shareId;
|
const canRedirect = !revisionId && !isMove && !shareId;
|
||||||
if (canRedirect) {
|
if (canRedirect) {
|
||||||
@ -187,5 +191,7 @@ class DataLoader extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(
|
||||||
inject("ui", "auth", "documents", "revisions", "policies")(DataLoader)
|
inject("ui", "auth", "documents", "revisions", "policies", "shares")(
|
||||||
|
DataLoader
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,12 @@ import { observer, inject } from "mobx-react";
|
|||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import { TableOfContentsIcon, EditIcon, PlusIcon } from "outline-icons";
|
import {
|
||||||
|
TableOfContentsIcon,
|
||||||
|
EditIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "outline-icons";
|
||||||
import { transparentize, darken } from "polished";
|
import { transparentize, darken } from "polished";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
import AuthStore from "stores/AuthStore";
|
import AuthStore from "stores/AuthStore";
|
||||||
@ -26,11 +31,13 @@ import Badge from "components/Badge";
|
|||||||
import Collaborators from "components/Collaborators";
|
import Collaborators from "components/Collaborators";
|
||||||
import { Action, Separator } from "components/Actions";
|
import { Action, Separator } from "components/Actions";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
|
import SharesStore from "stores/SharesStore";
|
||||||
import UiStore from "stores/UiStore";
|
import UiStore from "stores/UiStore";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
auth: AuthStore,
|
auth: AuthStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
|
shares: SharesStore,
|
||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
document: Document,
|
document: Document,
|
||||||
isDraft: boolean,
|
isDraft: boolean,
|
||||||
@ -82,9 +89,8 @@ class Header extends React.Component<Props> {
|
|||||||
|
|
||||||
handleShareLink = async (ev: SyntheticEvent<>) => {
|
handleShareLink = async (ev: SyntheticEvent<>) => {
|
||||||
const { document } = this.props;
|
const { document } = this.props;
|
||||||
if (!document.shareUrl) {
|
await document.share();
|
||||||
await document.share();
|
|
||||||
}
|
|
||||||
this.showShareModal = true;
|
this.showShareModal = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,6 +109,7 @@ class Header extends React.Component<Props> {
|
|||||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
shares,
|
||||||
document,
|
document,
|
||||||
policies,
|
policies,
|
||||||
isEditing,
|
isEditing,
|
||||||
@ -116,6 +123,8 @@ class Header extends React.Component<Props> {
|
|||||||
auth,
|
auth,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const share = shares.getByDocumentId(document.id);
|
||||||
|
const isPubliclyShared = share && share.published;
|
||||||
const can = policies.abilities(document.id);
|
const can = policies.abilities(document.id);
|
||||||
const canShareDocuments = auth.team && auth.team.sharing && can.share;
|
const canShareDocuments = auth.team && auth.team.sharing && can.share;
|
||||||
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
||||||
@ -181,22 +190,37 @@ class Header extends React.Component<Props> {
|
|||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Collaborators
|
<Fade>
|
||||||
document={document}
|
<Collaborators
|
||||||
currentUserId={auth.user ? auth.user.id : undefined}
|
document={document}
|
||||||
/>
|
currentUserId={auth.user ? auth.user.id : undefined}
|
||||||
{!isDraft &&
|
/>
|
||||||
!isEditing &&
|
</Fade>
|
||||||
|
{!isEditing &&
|
||||||
canShareDocuments && (
|
canShareDocuments && (
|
||||||
<Action>
|
<Action>
|
||||||
<Button
|
<Tooltip
|
||||||
onClick={this.handleShareLink}
|
tooltip={
|
||||||
title="Share document"
|
isPubliclyShared ? (
|
||||||
neutral
|
<React.Fragment>
|
||||||
small
|
Anyone with the link <br />can view this document
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
delay={500}
|
||||||
|
placement="bottom"
|
||||||
>
|
>
|
||||||
Share
|
<Button
|
||||||
</Button>
|
icon={isPubliclyShared ? <GlobeIcon /> : undefined}
|
||||||
|
onClick={this.handleShareLink}
|
||||||
|
neutral
|
||||||
|
small
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
@ -367,4 +391,4 @@ const Title = styled.div`
|
|||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default inject("auth", "ui", "policies")(Header);
|
export default inject("auth", "ui", "policies", "shares")(Header);
|
||||||
|
@ -1,28 +1,56 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
|
import { GlobeIcon, PadlockIcon } from "outline-icons";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import invariant from "invariant";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
|
import Flex from "components/Flex";
|
||||||
|
import Switch from "components/Switch";
|
||||||
import CopyToClipboard from "components/CopyToClipboard";
|
import CopyToClipboard from "components/CopyToClipboard";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
|
import SharesStore from "stores/SharesStore";
|
||||||
|
import UiStore from "stores/UiStore";
|
||||||
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
document: Document,
|
document: Document,
|
||||||
|
shares: SharesStore,
|
||||||
|
ui: UiStore,
|
||||||
|
policies: PoliciesStore,
|
||||||
onSubmit: () => void,
|
onSubmit: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class DocumentShare extends React.Component<Props> {
|
class DocumentShare extends React.Component<Props> {
|
||||||
@observable isCopied: boolean;
|
@observable isCopied: boolean;
|
||||||
|
@observable isSaving: boolean = false;
|
||||||
timeout: TimeoutID;
|
timeout: TimeoutID;
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePublishedChange = async event => {
|
||||||
|
const { document, shares } = this.props;
|
||||||
|
const share = shares.getByDocumentId(document.id);
|
||||||
|
invariant(share, "Share must exist");
|
||||||
|
|
||||||
|
this.isSaving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await share.save({ published: event.target.checked });
|
||||||
|
} catch (err) {
|
||||||
|
this.props.ui.showToast(err.message);
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleCopied = () => {
|
handleCopied = () => {
|
||||||
this.isCopied = true;
|
this.isCopied = true;
|
||||||
|
|
||||||
@ -33,35 +61,72 @@ class DocumentShare extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { document, onSubmit } = this.props;
|
const { document, policies, shares, onSubmit } = this.props;
|
||||||
|
const share = shares.getByDocumentId(document.id);
|
||||||
|
const can = policies.abilities(share ? share.id : "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
The link below allows anyone in the world to access a read-only
|
The link below provides a read-only version of the document{" "}
|
||||||
version of the document <strong>{document.title}</strong>. You can
|
<strong>{document.title}</strong>.{" "}
|
||||||
revoke this link in settings at any time.{" "}
|
{can.update &&
|
||||||
|
"You can optionally make it accessible to anyone with the link."}{" "}
|
||||||
<Link to="/settings/shares" onClick={onSubmit}>
|
<Link to="/settings/shares" onClick={onSubmit}>
|
||||||
Manage share links
|
Manage all share links
|
||||||
</Link>.
|
</Link>.
|
||||||
</HelpText>
|
</HelpText>
|
||||||
|
{can.update && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Switch
|
||||||
|
id="published"
|
||||||
|
label="Publish to internet"
|
||||||
|
onChange={this.handlePublishedChange}
|
||||||
|
checked={share ? share.published : false}
|
||||||
|
disabled={!share || this.isSaving}
|
||||||
|
/>
|
||||||
|
<Privacy>
|
||||||
|
{share.published ? <GlobeIcon /> : <PadlockIcon />}
|
||||||
|
<PrivacyText>
|
||||||
|
{share.published
|
||||||
|
? "Anyone with the link can view this document"
|
||||||
|
: "Only team members with access can view this document"}
|
||||||
|
</PrivacyText>
|
||||||
|
</Privacy>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
label="Share link"
|
label="Get link"
|
||||||
value={document.shareUrl || "Loading…"}
|
value={share ? share.url : "Loading…"}
|
||||||
|
labelHidden
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={document.shareUrl || ""}
|
text={share ? share.url : ""}
|
||||||
onCopy={this.handleCopied}
|
onCopy={this.handleCopied}
|
||||||
>
|
>
|
||||||
<Button type="submit" disabled={this.isCopied} primary>
|
<Button type="submit" disabled={this.isCopied || !share} primary>
|
||||||
{this.isCopied ? "Copied!" : "Copy Link"}
|
{this.isCopied ? "Copied!" : "Copy Link"}
|
||||||
</Button>
|
</Button>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard> <a href={share.url} target="_blank">
|
||||||
|
Preview
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocumentShare;
|
const Privacy = styled(Flex)`
|
||||||
|
flex-align: center;
|
||||||
|
margin-left: -4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PrivacyText = styled(HelpText)`
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default inject("shares", "ui", "policies")(DocumentShare);
|
||||||
|
@ -52,7 +52,7 @@ class Shares extends React.Component<Props> {
|
|||||||
<Subheading>Shared Documents</Subheading>
|
<Subheading>Shared Documents</Subheading>
|
||||||
{hasSharedDocuments ? (
|
{hasSharedDocuments ? (
|
||||||
<List>
|
<List>
|
||||||
{shares.orderedData.map(share => (
|
{shares.published.map(share => (
|
||||||
<ShareListItem key={share.id} share={share} />
|
<ShareListItem key={share.id} share={share} />
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
@ -33,11 +33,27 @@ const description = event => {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{capitalize(event.verbPastTense)} a{" "}
|
{capitalize(event.verbPastTense)} a{" "}
|
||||||
<Link to={`/share/${event.modelId || ""}`}>public link</Link> to the{" "}
|
<Link to={`/share/${event.modelId || ""}`}>share link</Link> to the{" "}
|
||||||
<Link to={`/doc/${event.documentId}`}>{event.data.name}</Link>{" "}
|
<Link to={`/doc/${event.documentId}`}>{event.data.name}</Link>{" "}
|
||||||
document
|
document
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
case "shares.update":
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{event.data.published ? (
|
||||||
|
<React.Fragment>
|
||||||
|
Published a document{" "}
|
||||||
|
<Link to={`/share/${event.modelId || ""}`}>share link</Link>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
Unpublished a document{" "}
|
||||||
|
<Link to={`/share/${event.modelId || ""}`}>share link</Link>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
case "users.create":
|
case "users.create":
|
||||||
return (
|
return (
|
||||||
<React.Fragment>{event.data.name} created an account</React.Fragment>
|
<React.Fragment>{event.data.name} created an account</React.Fragment>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { sortBy } from "lodash";
|
import invariant from "invariant";
|
||||||
|
import { sortBy, filter, find } from "lodash";
|
||||||
import { action, computed } from "mobx";
|
import { action, computed } from "mobx";
|
||||||
import { client } from "utils/ApiClient";
|
import { client } from "utils/ApiClient";
|
||||||
import BaseStore from "./BaseStore";
|
import BaseStore from "./BaseStore";
|
||||||
@ -7,7 +8,7 @@ import RootStore from "./RootStore";
|
|||||||
import Share from "models/Share";
|
import Share from "models/Share";
|
||||||
|
|
||||||
export default class SharesStore extends BaseStore<Share> {
|
export default class SharesStore extends BaseStore<Share> {
|
||||||
actions = ["list", "create"];
|
actions = ["info", "list", "create", "update"];
|
||||||
|
|
||||||
constructor(rootStore: RootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
super(rootStore, Share);
|
super(rootStore, Share);
|
||||||
@ -18,9 +19,44 @@ export default class SharesStore extends BaseStore<Share> {
|
|||||||
return sortBy(Array.from(this.data.values()), "createdAt").reverse();
|
return sortBy(Array.from(this.data.values()), "createdAt").reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get published(): Share[] {
|
||||||
|
return filter(this.orderedData, share => share.published);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
revoke = async (share: Share) => {
|
revoke = async (share: Share) => {
|
||||||
await client.post("/shares.revoke", { id: share.id });
|
await client.post("/shares.revoke", { id: share.id });
|
||||||
this.remove(share.id);
|
this.remove(share.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
async create(params: Object) {
|
||||||
|
let item = this.getByDocumentId(params.documentId);
|
||||||
|
if (item) return item;
|
||||||
|
|
||||||
|
return super.create(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async fetch(documentId: string, options?: Object = {}): Promise<*> {
|
||||||
|
let item = this.getByDocumentId(documentId);
|
||||||
|
if (item && !options.force) return item;
|
||||||
|
|
||||||
|
this.isFetching = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await client.post(`/${this.modelName}s.info`, { documentId });
|
||||||
|
invariant(res && res.data, "Data should be available");
|
||||||
|
|
||||||
|
this.addPolicies(res.policies);
|
||||||
|
return this.add(res.data);
|
||||||
|
} finally {
|
||||||
|
this.isFetching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getByDocumentId = (documentId): ?Share => {
|
||||||
|
return find(this.orderedData, share => share.documentId === documentId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@
|
|||||||
"mobx-react": "^5.4.2",
|
"mobx-react": "^5.4.2",
|
||||||
"natural-sort": "^1.0.0",
|
"natural-sort": "^1.0.0",
|
||||||
"nodemailer": "^4.4.0",
|
"nodemailer": "^4.4.0",
|
||||||
"outline-icons": "^1.19.0",
|
"outline-icons": "^1.20.0",
|
||||||
"oy-vey": "^0.10.0",
|
"oy-vey": "^0.10.0",
|
||||||
"pg": "^6.1.5",
|
"pg": "^6.1.5",
|
||||||
"pg-hstore": "2.3.2",
|
"pg-hstore": "2.3.2",
|
||||||
|
@ -9,6 +9,15 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`#shares.info should require authentication 1`] = `
|
||||||
|
Object {
|
||||||
|
"error": "authentication_required",
|
||||||
|
"message": "Authentication required",
|
||||||
|
"ok": false,
|
||||||
|
"status": 401,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`#shares.list should require authentication 1`] = `
|
exports[`#shares.list should require authentication 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
@ -26,3 +35,12 @@ Object {
|
|||||||
"status": 401,
|
"status": 401,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`#shares.update should require authentication 1`] = `
|
||||||
|
Object {
|
||||||
|
"error": "authentication_required",
|
||||||
|
"message": "Authentication required",
|
||||||
|
"ok": false,
|
||||||
|
"status": 401,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -391,7 +391,12 @@ async function loadDocument({ id, shareId, user }) {
|
|||||||
if (!share || share.document.archivedAt) {
|
if (!share || share.document.archivedAt) {
|
||||||
throw new InvalidRequestError("Document could not be found for shareId");
|
throw new InvalidRequestError("Document could not be found for shareId");
|
||||||
}
|
}
|
||||||
|
|
||||||
document = share.document;
|
document = share.document;
|
||||||
|
|
||||||
|
if (!share.published) {
|
||||||
|
authorize(user, "read", document);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
document = await Document.findByPk(
|
document = await Document.findByPk(
|
||||||
id,
|
id,
|
||||||
|
@ -3,8 +3,9 @@ import Router from "koa-router";
|
|||||||
import Sequelize from "sequelize";
|
import Sequelize from "sequelize";
|
||||||
import auth from "../middlewares/authentication";
|
import auth from "../middlewares/authentication";
|
||||||
import pagination from "./middlewares/pagination";
|
import pagination from "./middlewares/pagination";
|
||||||
import { presentShare } from "../presenters";
|
import { presentShare, presentPolicies } from "../presenters";
|
||||||
import { Document, User, Event, Share, Team } from "../models";
|
import { Document, User, Event, Share, Team } from "../models";
|
||||||
|
import { NotFoundError } from "../errors";
|
||||||
import policy from "../policies";
|
import policy from "../policies";
|
||||||
|
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
@ -12,15 +13,29 @@ const { authorize } = policy;
|
|||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post("shares.info", auth(), async ctx => {
|
router.post("shares.info", auth(), async ctx => {
|
||||||
const { id } = ctx.body;
|
const { id, documentId } = ctx.body;
|
||||||
ctx.assertUuid(id, "id is required");
|
ctx.assertUuid(id || documentId, "id or documentId is required");
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const share = await Share.findByPk(id);
|
const share = await Share.findOne({
|
||||||
|
where: id
|
||||||
|
? {
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
documentId,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!share) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
authorize(user, "read", share);
|
authorize(user, "read", share);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: presentShare(share),
|
data: presentShare(share),
|
||||||
|
policies: presentPolicies(user, [share]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,6 +47,7 @@ router.post("shares.list", auth(), pagination(), async ctx => {
|
|||||||
const where = {
|
const where = {
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
published: true,
|
||||||
revokedAt: { [Op.eq]: null },
|
revokedAt: { [Op.eq]: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,6 +73,11 @@ router.post("shares.list", auth(), pagination(), async ctx => {
|
|||||||
required: true,
|
required: true,
|
||||||
as: "user",
|
as: "user",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
model: Team,
|
||||||
|
required: true,
|
||||||
|
as: "team",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
offset: ctx.state.pagination.offset,
|
offset: ctx.state.pagination.offset,
|
||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
@ -65,6 +86,35 @@ router.post("shares.list", auth(), pagination(), async ctx => {
|
|||||||
ctx.body = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
data: shares.map(presentShare),
|
data: shares.map(presentShare),
|
||||||
|
policies: presentPolicies(user, shares),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("shares.update", auth(), async ctx => {
|
||||||
|
const { id, published } = ctx.body;
|
||||||
|
ctx.assertUuid(id, "id is required");
|
||||||
|
ctx.assertPresent(published, "published is required");
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
const share = await Share.findByPk(id);
|
||||||
|
authorize(user, "update", share);
|
||||||
|
|
||||||
|
share.published = published;
|
||||||
|
await share.save();
|
||||||
|
|
||||||
|
await Event.create({
|
||||||
|
name: "shares.update",
|
||||||
|
documentId: share.documentId,
|
||||||
|
modelId: share.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
actorId: user.id,
|
||||||
|
data: { published },
|
||||||
|
ip: ctx.request.ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: presentShare(share),
|
||||||
|
policies: presentPolicies(user, [share]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +128,7 @@ router.post("shares.create", auth(), async ctx => {
|
|||||||
authorize(user, "share", document);
|
authorize(user, "share", document);
|
||||||
authorize(user, "share", team);
|
authorize(user, "share", team);
|
||||||
|
|
||||||
const [share] = await Share.findOrCreate({
|
const [share, isCreated] = await Share.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -87,22 +137,26 @@ router.post("shares.create", auth(), async ctx => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await Event.create({
|
if (isCreated) {
|
||||||
name: "shares.create",
|
await Event.create({
|
||||||
documentId,
|
name: "shares.create",
|
||||||
collectionId: document.collectionId,
|
documentId,
|
||||||
modelId: share.id,
|
collectionId: document.collectionId,
|
||||||
teamId: user.teamId,
|
modelId: share.id,
|
||||||
actorId: user.id,
|
teamId: user.teamId,
|
||||||
data: { name: document.title },
|
actorId: user.id,
|
||||||
ip: ctx.request.ip,
|
data: { name: document.title },
|
||||||
});
|
ip: ctx.request.ip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
share.team = team;
|
||||||
share.user = user;
|
share.user = user;
|
||||||
share.document = document;
|
share.document = document;
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: presentShare(share),
|
data: presentShare(share),
|
||||||
|
policies: presentPolicies(user, [share]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,6 +52,24 @@ describe("#shares.list", async () => {
|
|||||||
expect(body.data.length).toEqual(0);
|
expect(body.data.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not return unpublished shares", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
await buildShare({
|
||||||
|
published: false,
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/shares.list", {
|
||||||
|
body: { token: user.getJwtToken() },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("admins should return shares created by all users", async () => {
|
it("admins should return shares created by all users", async () => {
|
||||||
const { user, admin, document } = await seed();
|
const { user, admin, document } = await seed();
|
||||||
const share = await buildShare({
|
const share = await buildShare({
|
||||||
@ -108,6 +126,7 @@ describe("#shares.create", async () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.published).toBe(false);
|
||||||
expect(body.data.documentTitle).toBe(document.title);
|
expect(body.data.documentTitle).toBe(document.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,6 +148,7 @@ describe("#shares.create", async () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.published).toBe(false);
|
||||||
expect(body.data.documentTitle).toBe(document.title);
|
expect(body.data.documentTitle).toBe(document.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -196,6 +216,157 @@ describe("#shares.create", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#shares.info", async () => {
|
||||||
|
it("should allow reading share by id", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/shares.info", {
|
||||||
|
body: { token: user.getJwtToken(), id: share.id },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).toBe(share.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow reading share by documentId", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/shares.info", {
|
||||||
|
body: { token: user.getJwtToken(), documentId: document.id },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).toBe(share.id);
|
||||||
|
expect(body.data.published).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not find share for different user", async () => {
|
||||||
|
const { admin, document } = await seed();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: admin.teamId,
|
||||||
|
});
|
||||||
|
await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: admin.teamId,
|
||||||
|
userId: admin.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.info", {
|
||||||
|
body: { token: user.getJwtToken(), documentId: document.id },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require authentication", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.info", {
|
||||||
|
body: { id: share.id },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(401);
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require authorization", async () => {
|
||||||
|
const { admin, document } = await seed();
|
||||||
|
const user = await buildUser();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: admin.teamId,
|
||||||
|
userId: admin.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.info", {
|
||||||
|
body: { token: user.getJwtToken(), id: share.id },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#shares.update", async () => {
|
||||||
|
it("should allow author to update a share", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/shares.update", {
|
||||||
|
body: { token: user.getJwtToken(), id: share.id, published: true },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).toBe(share.id);
|
||||||
|
expect(body.data.published).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow admin to update a share", async () => {
|
||||||
|
const { user, admin, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/shares.update", {
|
||||||
|
body: { token: admin.getJwtToken(), id: share.id, published: true },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).toBe(share.id);
|
||||||
|
expect(body.data.published).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require authentication", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.update", {
|
||||||
|
body: { id: share.id, published: true },
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(401);
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require authorization", async () => {
|
||||||
|
const { admin, document } = await seed();
|
||||||
|
const user = await buildUser();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: admin.teamId,
|
||||||
|
userId: admin.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.update", {
|
||||||
|
body: { token: user.getJwtToken(), id: share.id, published: true },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("#shares.revoke", async () => {
|
describe("#shares.revoke", async () => {
|
||||||
it("should allow author to revoke a share", async () => {
|
it("should allow author to revoke a share", async () => {
|
||||||
const { user, document } = await seed();
|
const { user, document } = await seed();
|
||||||
@ -242,10 +413,15 @@ describe("#shares.revoke", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should require authorization", async () => {
|
it("should require authorization", async () => {
|
||||||
const { document } = await seed();
|
const { admin, document } = await seed();
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const res = await server.post("/api/shares.create", {
|
const share = await buildShare({
|
||||||
body: { token: user.getJwtToken(), documentId: document.id },
|
documentId: document.id,
|
||||||
|
teamId: admin.teamId,
|
||||||
|
userId: admin.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/shares.revoke", {
|
||||||
|
body: { token: user.getJwtToken(), id: share.id },
|
||||||
});
|
});
|
||||||
expect(res.status).toEqual(403);
|
expect(res.status).toEqual(403);
|
||||||
});
|
});
|
||||||
|
20
server/migrations/20200723055414-add-published-to-shares.js
Normal file
20
server/migrations/20200723055414-add-published-to-shares.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('shares', 'published', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
update shares
|
||||||
|
set "published" = true
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('shares', 'published');
|
||||||
|
}
|
||||||
|
};
|
@ -80,6 +80,7 @@ Event.AUDIT_EVENTS = [
|
|||||||
"documents.move",
|
"documents.move",
|
||||||
"documents.delete",
|
"documents.delete",
|
||||||
"shares.create",
|
"shares.create",
|
||||||
|
"shares.update",
|
||||||
"shares.revoke",
|
"shares.revoke",
|
||||||
"groups.create",
|
"groups.create",
|
||||||
"groups.update",
|
"groups.update",
|
||||||
|
@ -9,6 +9,7 @@ const Share = sequelize.define(
|
|||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
|
published: DataTypes.BOOLEAN,
|
||||||
revokedAt: DataTypes.DATE,
|
revokedAt: DataTypes.DATE,
|
||||||
revokedById: DataTypes.UUID,
|
revokedById: DataTypes.UUID,
|
||||||
},
|
},
|
||||||
@ -30,10 +31,17 @@ Share.associate = models => {
|
|||||||
as: "team",
|
as: "team",
|
||||||
foreignKey: "teamId",
|
foreignKey: "teamId",
|
||||||
});
|
});
|
||||||
Share.belongsTo(models.Document, {
|
Share.belongsTo(models.Document.scope("withUnpublished"), {
|
||||||
as: "document",
|
as: "document",
|
||||||
foreignKey: "documentId",
|
foreignKey: "documentId",
|
||||||
});
|
});
|
||||||
|
Share.addScope("defaultScope", {
|
||||||
|
include: [
|
||||||
|
{ association: "user" },
|
||||||
|
{ association: "document" },
|
||||||
|
{ association: "team" },
|
||||||
|
],
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Share.prototype.revoke = function(userId) {
|
Share.prototype.revoke = function(userId) {
|
||||||
|
@ -6,8 +6,7 @@ import { AdminRequiredError } from "../errors";
|
|||||||
const { allow } = policy;
|
const { allow } = policy;
|
||||||
|
|
||||||
allow(User, ["read"], Share, (user, share) => user.teamId === share.teamId);
|
allow(User, ["read"], Share, (user, share) => user.teamId === share.teamId);
|
||||||
allow(User, ["update"], Share, (user, share) => false);
|
allow(User, ["update", "revoke"], Share, (user, share) => {
|
||||||
allow(User, ["revoke"], Share, (user, share) => {
|
|
||||||
if (!share || user.teamId !== share.teamId) return false;
|
if (!share || user.teamId !== share.teamId) return false;
|
||||||
if (user.id === share.userId) return true;
|
if (user.id === share.userId) return true;
|
||||||
if (user.isAdmin) return true;
|
if (user.isAdmin) return true;
|
||||||
|
@ -5,9 +5,11 @@ import { presentUser } from ".";
|
|||||||
export default function present(share: Share) {
|
export default function present(share: Share) {
|
||||||
return {
|
return {
|
||||||
id: share.id,
|
id: share.id,
|
||||||
|
documentId: share.documentId,
|
||||||
documentTitle: share.document.title,
|
documentTitle: share.document.title,
|
||||||
documentUrl: share.document.url,
|
documentUrl: share.document.url,
|
||||||
url: `${process.env.URL}/share/${share.id}`,
|
published: share.published,
|
||||||
|
url: `${share.team.url}/share/${share.id}`,
|
||||||
createdBy: presentUser(share.user),
|
createdBy: presentUser(share.user),
|
||||||
createdAt: share.createdAt,
|
createdAt: share.createdAt,
|
||||||
updatedAt: share.updatedAt,
|
updatedAt: share.updatedAt,
|
||||||
|
@ -24,7 +24,10 @@ export async function buildShare(overrides: Object = {}) {
|
|||||||
overrides.userId = user.id;
|
overrides.userId = user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Share.create(overrides);
|
return Share.create({
|
||||||
|
published: true,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTeam(overrides: Object = {}) {
|
export function buildTeam(overrides: Object = {}) {
|
||||||
|
@ -38,7 +38,7 @@ export default createGlobalStyle`
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: ${props => props.theme.primary};
|
color: ${props => props.theme.link};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ export const light = {
|
|||||||
background: colors.white,
|
background: colors.white,
|
||||||
secondaryBackground: colors.warmGrey,
|
secondaryBackground: colors.warmGrey,
|
||||||
|
|
||||||
link: colors.almostBlack,
|
link: colors.primary,
|
||||||
text: colors.almostBlack,
|
text: colors.almostBlack,
|
||||||
textSecondary: colors.slateDark,
|
textSecondary: colors.slateDark,
|
||||||
textTertiary: colors.slate,
|
textTertiary: colors.slate,
|
||||||
@ -161,7 +161,7 @@ export const dark = {
|
|||||||
background: colors.almostBlack,
|
background: colors.almostBlack,
|
||||||
secondaryBackground: colors.black50,
|
secondaryBackground: colors.black50,
|
||||||
|
|
||||||
link: colors.almostWhite,
|
link: "#137FFB",
|
||||||
text: colors.almostWhite,
|
text: colors.almostWhite,
|
||||||
textSecondary: lighten(0.1, colors.slate),
|
textSecondary: lighten(0.1, colors.slate),
|
||||||
textTertiary: colors.slate,
|
textTertiary: colors.slate,
|
||||||
|
@ -7171,10 +7171,10 @@ osenv@^0.1.4:
|
|||||||
os-homedir "^1.0.0"
|
os-homedir "^1.0.0"
|
||||||
os-tmpdir "^1.0.0"
|
os-tmpdir "^1.0.0"
|
||||||
|
|
||||||
outline-icons@^1.19.0, outline-icons@^1.19.1:
|
outline-icons@^1.19.1, outline-icons@^1.20.0:
|
||||||
version "1.19.1"
|
version "1.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.19.1.tgz#5251b966ae9987b18f060b58ce469d248c2313aa"
|
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.20.0.tgz#7d3814fade75ecd78492c9d9779183aed51502d8"
|
||||||
integrity sha512-YeGLDG7KgBvrDy+WOQUqN8rD3qO5u524E0Wj6ejaWNQ8/1ou6kkUrx65mk8LtESfKLMWZHuQG/u6onurY9rKIw==
|
integrity sha512-YZJyqxl47zgHS3QAsznP18TBOgM4UdbeKoU6Am+UlgqUdXZi5VC1NbR/NwLBflgOs490DjF2EVTcRbYbN2ZMLg==
|
||||||
|
|
||||||
oy-vey@^0.10.0:
|
oy-vey@^0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
|
Reference in New Issue
Block a user