Share Permissions (#761)
* Share restrictions * Tweak language, add spec
This commit is contained in:
45
app/components/Checkbox.js
Normal file
45
app/components/Checkbox.js
Normal file
@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import HelpText from 'components/HelpText';
|
||||
|
||||
export type Props = {
|
||||
checked?: boolean,
|
||||
label?: string,
|
||||
className?: string,
|
||||
note?: string,
|
||||
};
|
||||
|
||||
const LabelText = styled.span`
|
||||
font-weight: 500;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-bottom: 8px;
|
||||
`;
|
||||
|
||||
const Label = styled.label`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export default function Checkbox({
|
||||
label,
|
||||
note,
|
||||
className,
|
||||
short,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Wrapper>
|
||||
<Label>
|
||||
<input type="checkbox" {...rest} />
|
||||
{label && <LabelText>{label}</LabelText>}
|
||||
</Label>
|
||||
{note && <HelpText small>{note}</HelpText>}
|
||||
</Wrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import styled from 'styled-components';
|
||||
const HelpText = styled.p`
|
||||
margin-top: 0;
|
||||
color: ${props => props.theme.slateDark};
|
||||
font-size: ${props => (props.small ? '13px' : 'auto')};
|
||||
`;
|
||||
|
||||
export default HelpText;
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
DocumentIcon,
|
||||
ProfileIcon,
|
||||
SettingsIcon,
|
||||
PadlockIcon,
|
||||
CodeIcon,
|
||||
UserIcon,
|
||||
LinkIcon,
|
||||
@ -61,6 +62,11 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
Details
|
||||
</SidebarLink>
|
||||
)}
|
||||
{user.isAdmin && (
|
||||
<SidebarLink to="/settings/security" icon={<PadlockIcon />}>
|
||||
Security
|
||||
</SidebarLink>
|
||||
)}
|
||||
<SidebarLink to="/settings/people" icon={<UserIcon />}>
|
||||
People
|
||||
</SidebarLink>
|
||||
|
@ -49,7 +49,7 @@ const Container = styled.li`
|
||||
align-items: center;
|
||||
animation: ${fadeAndScaleIn} 100ms ease;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
padding: 10px 12px;
|
||||
color: ${props => props.theme.white};
|
||||
background: ${props => props.theme[props.type]};
|
||||
font-size: 15px;
|
||||
|
@ -6,11 +6,13 @@ import { MoreIcon } from 'outline-icons';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import { documentMoveUrl } from 'utils/routeHelpers';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
|
||||
type Props = {
|
||||
ui: UiStore,
|
||||
auth: AuthStore,
|
||||
label?: React.Node,
|
||||
history: Object,
|
||||
document: Document,
|
||||
@ -69,7 +71,8 @@ class DocumentMenu extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { document, label, className, showPrint } = this.props;
|
||||
const { document, label, className, showPrint, auth } = this.props;
|
||||
const canShareDocuments = auth.team && auth.team.sharing;
|
||||
|
||||
return (
|
||||
<DropdownMenu label={label || <MoreIcon />} className={className}>
|
||||
@ -91,12 +94,14 @@ class DocumentMenu extends React.Component<Props> {
|
||||
Star
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canShareDocuments && (
|
||||
<DropdownMenuItem
|
||||
onClick={this.handleShareLink}
|
||||
title="Create a public share link"
|
||||
>
|
||||
Share link…
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<hr />
|
||||
<DropdownMenuItem
|
||||
onClick={this.handleNewChild}
|
||||
@ -123,4 +128,4 @@ class DocumentMenu extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(inject('ui')(DocumentMenu));
|
||||
export default withRouter(inject('ui', 'auth')(DocumentMenu));
|
||||
|
@ -11,6 +11,7 @@ import Document from 'scenes/Document';
|
||||
import Search from 'scenes/Search';
|
||||
import Settings from 'scenes/Settings';
|
||||
import Details from 'scenes/Settings/Details';
|
||||
import Security from 'scenes/Settings/Security';
|
||||
import People from 'scenes/Settings/People';
|
||||
import Slack from 'scenes/Settings/Slack';
|
||||
import Shares from 'scenes/Settings/Shares';
|
||||
@ -43,6 +44,7 @@ export default function Routes() {
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/details" component={Details} />
|
||||
<Route exact path="/settings/security" component={Security} />
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/shares" component={Shares} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
|
@ -2,11 +2,12 @@
|
||||
import * as React from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
import Document from 'models/Document';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import { documentEditUrl } from 'utils/routeHelpers';
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
@ -32,6 +33,7 @@ type Props = {
|
||||
autosave?: boolean,
|
||||
}) => *,
|
||||
history: Object,
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
@ -90,7 +92,9 @@ class Header extends React.Component<Props> {
|
||||
isPublishing,
|
||||
isSaving,
|
||||
savingIsDisabled,
|
||||
auth,
|
||||
} = this.props;
|
||||
const canShareDocuments = auth.team && auth.team.sharing;
|
||||
|
||||
return (
|
||||
<Actions
|
||||
@ -134,7 +138,8 @@ class Header extends React.Component<Props> {
|
||||
</Action>
|
||||
)}
|
||||
{!isDraft &&
|
||||
!isEditing && (
|
||||
!isEditing &&
|
||||
canShareDocuments && (
|
||||
<Action>
|
||||
<Link onClick={this.handleShareLink} title="Share document">
|
||||
Share
|
||||
@ -251,4 +256,4 @@ const Link = styled.a`
|
||||
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
export default inject('auth')(Header);
|
||||
|
@ -44,7 +44,7 @@ class Details extends React.Component<Props> {
|
||||
name: this.name,
|
||||
avatarUrl: this.avatarUrl,
|
||||
});
|
||||
this.props.ui.showToast('Details saved', 'success');
|
||||
this.props.ui.showToast('Settings saved', 'success');
|
||||
};
|
||||
|
||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||
|
80
app/scenes/Settings/Security.js
Normal file
80
app/scenes/Settings/Security.js
Normal file
@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import Checkbox from 'components/Checkbox';
|
||||
import Button from 'components/Button';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import HelpText from 'components/HelpText';
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Security extends React.Component<Props> {
|
||||
form: ?HTMLFormElement;
|
||||
|
||||
@observable sharing: boolean;
|
||||
|
||||
componentDidMount() {
|
||||
const { auth } = this.props;
|
||||
if (auth.team) {
|
||||
this.sharing = auth.team.sharing;
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
await this.props.auth.updateTeam({
|
||||
sharing: this.sharing,
|
||||
});
|
||||
this.props.ui.showToast('Settings saved', 'success');
|
||||
};
|
||||
|
||||
handleChange = (ev: SyntheticInputEvent<*>) => {
|
||||
if (ev.target.name === 'sharing') {
|
||||
this.sharing = ev.target.checked;
|
||||
}
|
||||
};
|
||||
|
||||
get isValid() {
|
||||
return this.form && this.form.checkValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSaving } = this.props.auth;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Security" />
|
||||
<h1>Security</h1>
|
||||
<HelpText>
|
||||
Settings that impact the access, security and privacy of your
|
||||
knowledgebase.
|
||||
</HelpText>
|
||||
|
||||
<form onSubmit={this.handleSubmit} ref={ref => (this.form = ref)}>
|
||||
<Checkbox
|
||||
label="Allow sharing documents"
|
||||
name="sharing"
|
||||
checked={this.sharing}
|
||||
onChange={this.handleChange}
|
||||
note="When enabled documents can be shared publicly by any team member"
|
||||
/>
|
||||
<Button type="submit" disabled={isSaving || !this.isValid}>
|
||||
{isSaving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</form>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('auth', 'ui')(Security);
|
@ -1,7 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SharesStore from 'stores/SharesStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
import ShareListItem from './components/ShareListItem';
|
||||
import List from 'components/List';
|
||||
@ -11,6 +13,7 @@ import HelpText from 'components/HelpText';
|
||||
|
||||
type Props = {
|
||||
shares: SharesStore,
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
@ -20,7 +23,9 @@ class Shares extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { shares } = this.props;
|
||||
const { shares, auth } = this.props;
|
||||
const { user } = auth;
|
||||
const canShareDocuments = auth.team && auth.team.sharing;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
@ -31,7 +36,16 @@ class Shares extends React.Component<Props> {
|
||||
can access a read-only version of the document until the link has been
|
||||
revoked.
|
||||
</HelpText>
|
||||
|
||||
{user &&
|
||||
user.isAdmin && (
|
||||
<HelpText>
|
||||
{!canShareDocuments && (
|
||||
<strong>Sharing is currently disabled.</strong>
|
||||
)}{' '}
|
||||
You can turn {canShareDocuments ? 'off' : 'on'} public document
|
||||
sharing in <Link to="/settings/security">security settings</Link>.
|
||||
</HelpText>
|
||||
)}
|
||||
<List>
|
||||
{shares.orderedData.map(share => (
|
||||
<ShareListItem key={share.id} share={share} />
|
||||
@ -42,4 +56,4 @@ class Shares extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('shares')(Shares);
|
||||
export default inject('shares', 'auth')(Shares);
|
||||
|
@ -78,7 +78,11 @@ class AuthStore {
|
||||
};
|
||||
|
||||
@action
|
||||
updateTeam = async (params: { name: string, avatarUrl: ?string }) => {
|
||||
updateTeam = async (params: {
|
||||
name?: string,
|
||||
avatarUrl?: ?string,
|
||||
sharing?: boolean,
|
||||
}) => {
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
|
@ -31,6 +31,7 @@ export type Team = {
|
||||
avatarUrl: string,
|
||||
slackConnected: boolean,
|
||||
googleConnected: boolean,
|
||||
sharing: boolean,
|
||||
};
|
||||
|
||||
export type NavigationNode = {
|
||||
|
160
flow-typed/npm/react-router-dom_v4.x.x.js
vendored
160
flow-typed/npm/react-router-dom_v4.x.x.js
vendored
@ -1,160 +0,0 @@
|
||||
// flow-typed signature: cf916fca23433d4bbcb7a75f2604407d
|
||||
// flow-typed version: f821d89401/react-router-dom_v4.x.x/flow_>=v0.53.x
|
||||
|
||||
declare module "react-router-dom" {
|
||||
declare export class BrowserRouter extends React$Component<{
|
||||
basename?: string,
|
||||
forceRefresh?: boolean,
|
||||
getUserConfirmation?: GetUserConfirmation,
|
||||
keyLength?: number,
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class HashRouter extends React$Component<{
|
||||
basename?: string,
|
||||
getUserConfirmation?: GetUserConfirmation,
|
||||
hashType?: "slash" | "noslash" | "hashbang",
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class Link extends React$Component<{
|
||||
className?: string,
|
||||
to: string | LocationShape,
|
||||
replace?: boolean,
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class NavLink extends React$Component<{
|
||||
to: string | LocationShape,
|
||||
activeClassName?: string,
|
||||
className?: string,
|
||||
activeStyle?: Object,
|
||||
style?: Object,
|
||||
isActive?: (match: Match, location: Location) => boolean,
|
||||
children?: React$Node,
|
||||
exact?: boolean,
|
||||
strict?: boolean
|
||||
}> {}
|
||||
|
||||
// NOTE: Below are duplicated from react-router. If updating these, please
|
||||
// update the react-router and react-router-native types as well.
|
||||
declare export type Location = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
state?: any,
|
||||
key?: string
|
||||
};
|
||||
|
||||
declare export type LocationShape = {
|
||||
pathname?: string,
|
||||
search?: string,
|
||||
hash?: string,
|
||||
state?: any
|
||||
};
|
||||
|
||||
declare export type HistoryAction = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type RouterHistory = {
|
||||
length: number,
|
||||
location: Location,
|
||||
action: HistoryAction,
|
||||
listen(
|
||||
callback: (location: Location, action: HistoryAction) => void
|
||||
): () => void,
|
||||
push(path: string | LocationShape, state?: any): void,
|
||||
replace(path: string | LocationShape, state?: any): void,
|
||||
go(n: number): void,
|
||||
goBack(): void,
|
||||
goForward(): void,
|
||||
canGo?: (n: number) => boolean,
|
||||
block(
|
||||
callback: (location: Location, action: HistoryAction) => boolean
|
||||
): void,
|
||||
// createMemoryHistory
|
||||
index?: number,
|
||||
entries?: Array<Location>
|
||||
};
|
||||
|
||||
declare export type Match = {
|
||||
params: { [key: string]: ?string },
|
||||
isExact: boolean,
|
||||
path: string,
|
||||
url: string
|
||||
};
|
||||
|
||||
declare export type ContextRouter = {|
|
||||
history: RouterHistory,
|
||||
location: Location,
|
||||
match: Match,
|
||||
staticContext?: StaticRouterContext,
|
||||
|};
|
||||
|
||||
declare export type GetUserConfirmation = (
|
||||
message: string,
|
||||
callback: (confirmed: boolean) => void
|
||||
) => void;
|
||||
|
||||
declare type StaticRouterContext = {
|
||||
url?: string
|
||||
};
|
||||
|
||||
declare export class StaticRouter extends React$Component<{
|
||||
basename?: string,
|
||||
location?: string | Location,
|
||||
context: StaticRouterContext,
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class MemoryRouter extends React$Component<{
|
||||
initialEntries?: Array<LocationShape | string>,
|
||||
initialIndex?: number,
|
||||
getUserConfirmation?: GetUserConfirmation,
|
||||
keyLength?: number,
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class Router extends React$Component<{
|
||||
history: RouterHistory,
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export class Prompt extends React$Component<{
|
||||
message: string | ((location: Location) => string | boolean),
|
||||
when?: boolean
|
||||
}> {}
|
||||
|
||||
declare export class Redirect extends React$Component<{
|
||||
to: string | LocationShape,
|
||||
push?: boolean
|
||||
}> {}
|
||||
|
||||
declare export class Route extends React$Component<{
|
||||
component?: React$ComponentType<*>,
|
||||
render?: (router: ContextRouter) => React$Node,
|
||||
children?: React$ComponentType<ContextRouter> | React$Node,
|
||||
path?: string,
|
||||
exact?: boolean,
|
||||
strict?: boolean
|
||||
}> {}
|
||||
|
||||
declare export class Switch extends React$Component<{
|
||||
children?: React$Node
|
||||
}> {}
|
||||
|
||||
declare export function withRouter<P>(
|
||||
Component: React$ComponentType<{| ...ContextRouter, ...P |}>
|
||||
): React$ComponentType<P>;
|
||||
|
||||
declare type MatchPathOptions = {
|
||||
path?: string,
|
||||
exact?: boolean,
|
||||
sensitive?: boolean,
|
||||
strict?: boolean
|
||||
};
|
||||
|
||||
declare export function matchPath(
|
||||
pathname: string,
|
||||
options?: MatchPathOptions | string
|
||||
): null | Match;
|
||||
}
|
@ -4,7 +4,7 @@ import Sequelize from 'sequelize';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentShare } from '../presenters';
|
||||
import { Document, User, Share } from '../models';
|
||||
import { Document, User, Share, Team } from '../models';
|
||||
import policy from '../policies';
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
@ -57,7 +57,9 @@ router.post('shares.create', auth(), async ctx => {
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(documentId);
|
||||
const team = await Team.findById(user.teamId);
|
||||
authorize(user, 'share', document);
|
||||
authorize(user, 'share', team);
|
||||
|
||||
const [share] = await Share.findOrCreate({
|
||||
where: {
|
||||
|
@ -122,6 +122,15 @@ describe('#shares.create', async () => {
|
||||
expect(body.data.id).toBe(share.id);
|
||||
});
|
||||
|
||||
it('should not allow creating a share record if disabled', async () => {
|
||||
const { user, document, team } = await seed();
|
||||
await team.update({ sharing: false });
|
||||
const res = await server.post('/api/shares.create', {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { document } = await seed();
|
||||
const res = await server.post('/api/shares.create', {
|
||||
|
@ -12,7 +12,7 @@ const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post('team.update', auth(), async ctx => {
|
||||
const { name, avatarUrl } = ctx.body;
|
||||
const { name, avatarUrl, sharing } = ctx.body;
|
||||
const endpoint = publicS3Endpoint();
|
||||
|
||||
const user = ctx.state.user;
|
||||
@ -20,6 +20,7 @@ router.post('team.update', auth(), async ctx => {
|
||||
authorize(user, 'update', team);
|
||||
|
||||
if (name) team.name = name;
|
||||
if (sharing !== undefined) team.sharing = sharing;
|
||||
if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) {
|
||||
team.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
12
server/migrations/20180819054252-disable-sharing.js
Normal file
12
server/migrations/20180819054252-disable-sharing.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('teams', 'sharing', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
});
|
||||
},
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('teams', 'sharing');
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ const Team = sequelize.define(
|
||||
slackId: { type: DataTypes.STRING, allowNull: true },
|
||||
googleId: { type: DataTypes.STRING, allowNull: true },
|
||||
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
||||
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
||||
slackData: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
@ -39,11 +40,16 @@ const uploadAvatar = async model => {
|
||||
const endpoint = publicS3Endpoint();
|
||||
|
||||
if (model.avatarUrl && !model.avatarUrl.startsWith(endpoint)) {
|
||||
try {
|
||||
const newUrl = await uploadToS3FromUrl(
|
||||
model.avatarUrl,
|
||||
`avatars/${model.id}/${uuid.v4()}`
|
||||
);
|
||||
if (newUrl) model.avatarUrl = newUrl;
|
||||
} catch (err) {
|
||||
// we can try again next time
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,11 @@ const { allow } = policy;
|
||||
|
||||
allow(User, 'read', Team, (user, team) => team && user.teamId === team.id);
|
||||
|
||||
allow(User, 'share', Team, (user, team) => {
|
||||
if (!team || user.teamId !== team.id) return false;
|
||||
return team.sharing;
|
||||
});
|
||||
|
||||
allow(User, ['update', 'export'], Team, (user, team) => {
|
||||
if (!team || user.teamId !== team.id) return false;
|
||||
if (user.isAdmin) return true;
|
||||
|
@ -11,6 +11,7 @@ function present(ctx: Object, team: Team) {
|
||||
team.avatarUrl || (team.slackData ? team.slackData.image_88 : null),
|
||||
slackConnected: !!team.slackId,
|
||||
googleConnected: !!team.googleId,
|
||||
sharing: team.sharing,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ const theme = {
|
||||
placeholder: '#b1becc',
|
||||
danger: '#D0021B',
|
||||
warning: '#f08a24',
|
||||
success: '#1AB6FF',
|
||||
success: '#2f3336',
|
||||
info: '#a0d3e8',
|
||||
|
||||
slate: '#9BA6B2',
|
||||
|
Reference in New Issue
Block a user