mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add ReactIcons (#149)
This commit is contained in:
committed by
GitHub
parent
4c73b398e8
commit
6198d814d8
@@ -1,6 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import I18n from 'i18n-js';
|
import I18n from 'i18n-js';
|
||||||
import Button from '../common/Button';
|
import Button from '../common/Button';
|
||||||
|
import Switch from '../common/Switch';
|
||||||
|
import ActionLink from '../common/ActionLink';
|
||||||
|
import { CancelIcon } from '../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -61,34 +64,23 @@ class CommentEditForm extends React.Component<Props, State> {
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
isPowerUser ?
|
isPowerUser &&
|
||||||
<>
|
<Switch
|
||||||
<input
|
htmlId={`isPostUpdateFlagComment${id}`}
|
||||||
id={`isPostUpdateFlagComment${id}`}
|
onClick={e => this.handleCommentIsPostUpdateChange(!isPostUpdate)}
|
||||||
type="checkbox"
|
checked={isPostUpdate || false}
|
||||||
onChange={e => this.handleCommentIsPostUpdateChange(e.target.checked)}
|
label={I18n.t('post.new_comment.is_post_update')}
|
||||||
checked={isPostUpdate || false}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<label htmlFor={`isPostUpdateFlagComment${id}`}>
|
|
||||||
{I18n.t('post.new_comment.is_post_update')}
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="editCommentFormActions">
|
||||||
<a className="commentLink" onClick={toggleEditMode}>
|
<ActionLink onClick={toggleEditMode} icon={<CancelIcon />}>
|
||||||
{ I18n.t('common.buttons.cancel') }
|
{I18n.t('common.buttons.cancel')}
|
||||||
</a>
|
</ActionLink>
|
||||||
|
|
||||||
<Button
|
<Button onClick={() => handleUpdateComment(body, isPostUpdate)}>
|
||||||
onClick={() => handleUpdateComment(body, isPostUpdate)}
|
{I18n.t('common.buttons.update')}
|
||||||
>
|
|
||||||
{ I18n.t('common.buttons.update') }
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Separator from '../common/Separator';
|
|||||||
import { MutedText } from '../common/CustomTexts';
|
import { MutedText } from '../common/CustomTexts';
|
||||||
import friendlyDate from '../../helpers/datetime';
|
import friendlyDate from '../../helpers/datetime';
|
||||||
import { ReplyFormState } from '../../reducers/replyFormReducer';
|
import { ReplyFormState } from '../../reducers/replyFormReducer';
|
||||||
|
import ActionLink from '../common/ActionLink';
|
||||||
|
import { CancelIcon, DeleteIcon, EditIcon, ReplyIcon } from '../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -34,43 +36,43 @@ const CommentFooter = ({
|
|||||||
toggleEditMode,
|
toggleEditMode,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div className="commentFooter">
|
<div className="commentFooter">
|
||||||
<a className="commentReplyButton commentLink" onClick={handleToggleCommentReply}>
|
<ActionLink
|
||||||
|
onClick={handleToggleCommentReply}
|
||||||
|
icon={replyForm.isOpen ? <CancelIcon /> : <ReplyIcon />}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
replyForm.isOpen ?
|
replyForm.isOpen ?
|
||||||
I18n.t('common.buttons.cancel')
|
I18n.t('common.buttons.cancel')
|
||||||
:
|
:
|
||||||
I18n.t('post.comments.reply_button')
|
I18n.t('post.comments.reply_button')
|
||||||
}
|
}
|
||||||
</a>
|
</ActionLink>
|
||||||
{
|
{
|
||||||
isPowerUser || currentUserEmail === commentAuthorEmail ?
|
isPowerUser || currentUserEmail === commentAuthorEmail ?
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<ActionLink onClick={toggleEditMode} icon={<EditIcon />}>
|
||||||
<a onClick={toggleEditMode} className="commentLink">
|
|
||||||
{I18n.t('common.buttons.edit')}
|
{I18n.t('common.buttons.edit')}
|
||||||
</a>
|
</ActionLink>
|
||||||
|
|
||||||
<Separator />
|
<ActionLink
|
||||||
<a
|
|
||||||
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteComment(id)}
|
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteComment(id)}
|
||||||
className="commentLink">
|
icon={<DeleteIcon />}
|
||||||
{I18n.t('common.buttons.delete')}
|
>
|
||||||
</a>
|
{I18n.t('common.buttons.delete')}
|
||||||
|
</ActionLink>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<Separator />
|
|
||||||
<MutedText>{friendlyDate(createdAt)}</MutedText>
|
<MutedText>{friendlyDate(createdAt)}</MutedText>
|
||||||
|
|
||||||
{
|
{
|
||||||
createdAt !== updatedAt ?
|
createdAt !== updatedAt &&
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MutedText>{ I18n.t('common.edited').toLowerCase() }</MutedText>
|
<MutedText>{ I18n.t('common.edited').toLowerCase() }</MutedText>
|
||||||
</>
|
</>
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import I18n from 'i18n-js';
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
import { MutedText } from '../common/CustomTexts';
|
import { MutedText } from '../common/CustomTexts';
|
||||||
|
import Switch from '../common/Switch';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postUpdateFlagValue: boolean;
|
postUpdateFlagValue: boolean;
|
||||||
@@ -13,21 +14,15 @@ const NewCommentUpdateSection = ({
|
|||||||
handlePostUpdateFlag,
|
handlePostUpdateFlag,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div className="commentIsUpdateForm">
|
<div className="commentIsUpdateForm">
|
||||||
<div>
|
<Switch
|
||||||
<input
|
htmlId="isPostUpdateFlag"
|
||||||
id="isPostUpdateFlag"
|
onClick={handlePostUpdateFlag}
|
||||||
type="checkbox"
|
checked={postUpdateFlagValue || false}
|
||||||
onChange={handlePostUpdateFlag}
|
label={I18n.t('post.new_comment.is_post_update')}
|
||||||
checked={postUpdateFlagValue || false}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<label htmlFor="isPostUpdateFlag">{I18n.t('post.new_comment.is_post_update')}</label>
|
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
postUpdateFlagValue ?
|
postUpdateFlagValue &&
|
||||||
<MutedText>{I18n.t('post.new_comment.user_notification')}</MutedText>
|
<MutedText>{I18n.t('post.new_comment.user_notification')}</MutedText>
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import IPostStatus from '../../interfaces/IPostStatus';
|
|||||||
import IBoard from '../../interfaces/IBoard';
|
import IBoard from '../../interfaces/IBoard';
|
||||||
import Button from '../common/Button';
|
import Button from '../common/Button';
|
||||||
import Spinner from '../common/Spinner';
|
import Spinner from '../common/Spinner';
|
||||||
|
import ActionLink from '../common/ActionLink';
|
||||||
|
import { CancelIcon } from '../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -93,9 +95,9 @@ const PostEditForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="postEditFormButtons">
|
<div className="postEditFormButtons">
|
||||||
<a onClick={toggleEditMode}>
|
<ActionLink onClick={toggleEditMode} icon={<CancelIcon />}>
|
||||||
{ I18n.t('common.buttons.cancel') }
|
{I18n.t('common.buttons.cancel')}
|
||||||
</a>
|
</ActionLink>
|
||||||
|
|
||||||
<Button onClick={() => handleUpdatePost(title, description, boardId, postStatusId)}>
|
<Button onClick={() => handleUpdatePost(title, description, boardId, postStatusId)}>
|
||||||
{ isUpdating ? <Spinner /> : I18n.t('common.buttons.update') }
|
{ isUpdating ? <Spinner /> : I18n.t('common.buttons.update') }
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import I18n from 'i18n-js';
|
|||||||
import { MutedText } from '../common/CustomTexts';
|
import { MutedText } from '../common/CustomTexts';
|
||||||
import friendlyDate from '../../helpers/datetime';
|
import friendlyDate from '../../helpers/datetime';
|
||||||
import Separator from '../common/Separator';
|
import Separator from '../common/Separator';
|
||||||
|
import ActionLink from '../common/ActionLink';
|
||||||
|
import { DeleteIcon, EditIcon } from '../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -27,26 +29,29 @@ const PostFooter = ({
|
|||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div className="postFooter">
|
<div className="postFooter">
|
||||||
<div className="postAuthor">
|
<div className="postAuthor">
|
||||||
<span>{ I18n.t('post.published_by').toLowerCase() } </span>
|
<span>{I18n.t('post.published_by').toLowerCase()} </span>
|
||||||
<Gravatar email={authorEmail} size={24} className="postAuthorAvatar" />
|
<Gravatar email={authorEmail} size={24} className="postAuthorAvatar" />
|
||||||
{authorFullName}
|
{authorFullName}
|
||||||
|
<Separator />
|
||||||
|
{friendlyDate(createdAt)}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
isPowerUser || authorEmail === currentUserEmail ?
|
isPowerUser || authorEmail === currentUserEmail ?
|
||||||
<>
|
<div className="postFooterActions">
|
||||||
<a onClick={toggleEditMode}>
|
<ActionLink onClick={toggleEditMode} icon={<EditIcon />}>
|
||||||
{ I18n.t('common.buttons.edit') }
|
{I18n.t('common.buttons.edit')}
|
||||||
</a>
|
</ActionLink>
|
||||||
<Separator />
|
|
||||||
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDeletePost()}>
|
<ActionLink
|
||||||
{ I18n.t('common.buttons.delete') }
|
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeletePost()}
|
||||||
</a>
|
icon={<DeleteIcon />}
|
||||||
<Separator />
|
>
|
||||||
</>
|
{I18n.t('common.buttons.delete')}
|
||||||
|
</ActionLink>
|
||||||
|
</div>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<MutedText>{friendlyDate(createdAt)}</MutedText>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { IOAuth } from '../../../interfaces/IOAuth';
|
|||||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Separator from '../../common/Separator';
|
import Separator from '../../common/Separator';
|
||||||
|
import ActionLink from '../../common/ActionLink';
|
||||||
|
import { BackIcon } from '../../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedOAuth: IOAuth;
|
selectedOAuth: IOAuth;
|
||||||
@@ -88,16 +90,18 @@ const OAuthForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a
|
<ActionLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let confirmation = true;
|
let confirmation = true;
|
||||||
if (isDirty)
|
if (isDirty)
|
||||||
confirmation = confirm(I18n.t('common.unsaved_changes') + ' ' + I18n.t('common.confirmation'));
|
confirmation = confirm(I18n.t('common.unsaved_changes') + ' ' + I18n.t('common.confirmation'));
|
||||||
if (confirmation) setPage('index');
|
if (confirmation) setPage('index');
|
||||||
}}
|
}}
|
||||||
className="backButton link">
|
icon={<BackIcon />}
|
||||||
← { I18n.t('common.buttons.back') }
|
customClass="backButton"
|
||||||
</a>
|
>
|
||||||
|
{I18n.t('common.buttons.back')}
|
||||||
|
</ActionLink>
|
||||||
<h2>{ I18n.t(`site_settings.authentication.form.title_${page}`) }</h2>
|
<h2>{ I18n.t(`site_settings.authentication.form.title_${page}`) }</h2>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Switch from '../../common/Switch';
|
|||||||
import Separator from '../../common/Separator';
|
import Separator from '../../common/Separator';
|
||||||
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
|
||||||
import CopyToClipboardButton from '../../common/CopyToClipboardButton';
|
import CopyToClipboardButton from '../../common/CopyToClipboardButton';
|
||||||
|
import ActionLink from '../../common/ActionLink';
|
||||||
|
import { DeleteIcon, EditIcon, TestIcon } from '../../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
oAuth: IOAuth;
|
oAuth: IOAuth;
|
||||||
@@ -44,23 +46,32 @@ const OAuthProviderItem = ({
|
|||||||
label={I18n.t('site_settings.authentication.copy_url')}
|
label={I18n.t('site_settings.authentication.copy_url')}
|
||||||
textToCopy={oAuth.callbackUrl}
|
textToCopy={oAuth.callbackUrl}
|
||||||
/>
|
/>
|
||||||
<Separator />
|
|
||||||
<a onClick={() =>
|
<ActionLink
|
||||||
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
|
onClick={() =>
|
||||||
}>
|
window.open(`/o_auths/${oAuth.id}/start?reason=test`, '', 'width=640, height=640')
|
||||||
Test
|
}
|
||||||
</a>
|
icon={<TestIcon />}
|
||||||
<Separator />
|
>
|
||||||
<a onClick={() => {
|
{I18n.t('common.buttons.test')}
|
||||||
setSelectedOAuth(oAuth.id);
|
</ActionLink>
|
||||||
setPage('edit');
|
|
||||||
}}>
|
<ActionLink
|
||||||
{ I18n.t('common.buttons.edit') }
|
onClick={() => {
|
||||||
</a>
|
setSelectedOAuth(oAuth.id);
|
||||||
<Separator />
|
setPage('edit');
|
||||||
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}>
|
}}
|
||||||
{ I18n.t('common.buttons.delete') }
|
icon={<EditIcon />}
|
||||||
</a>
|
>
|
||||||
|
{I18n.t('common.buttons.edit')}
|
||||||
|
</ActionLink>
|
||||||
|
|
||||||
|
<ActionLink
|
||||||
|
onClick={() => confirm(I18n.t('common.confirmation')) && handleDeleteOAuth(oAuth.id)}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
>
|
||||||
|
{I18n.t('common.buttons.delete')}
|
||||||
|
</ActionLink>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import DragZone from '../../common/DragZone';
|
|||||||
import PostBoardLabel from '../../common/PostBoardLabel';
|
import PostBoardLabel from '../../common/PostBoardLabel';
|
||||||
import Separator from '../../common/Separator';
|
import Separator from '../../common/Separator';
|
||||||
import BoardForm from './BoardForm';
|
import BoardForm from './BoardForm';
|
||||||
|
import ActionLink from '../../common/ActionLink';
|
||||||
|
import { CancelIcon, DeleteIcon, EditIcon } from '../../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -83,13 +85,16 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="boardEditableActions">
|
<div className="boardEditableActions">
|
||||||
<a onClick={this.toggleEditMode}>{I18n.t('common.buttons.edit')}</a>
|
<ActionLink onClick={this.toggleEditMode} icon={<EditIcon />}>
|
||||||
|
{I18n.t('common.buttons.edit')}
|
||||||
|
</ActionLink>
|
||||||
|
|
||||||
<Separator />
|
<ActionLink
|
||||||
|
onClick={() => confirm(I18n.t('common.confirmation')) && handleDelete(id)}
|
||||||
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDelete(id)}>
|
icon={<DeleteIcon />}
|
||||||
|
>
|
||||||
{I18n.t('common.buttons.delete')}
|
{I18n.t('common.buttons.delete')}
|
||||||
</a>
|
</ActionLink>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
@@ -102,11 +107,12 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
handleUpdate={this.handleUpdate}
|
handleUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
<ActionLink
|
||||||
className="boardFormCancelButton"
|
onClick={this.toggleEditMode}
|
||||||
onClick={this.toggleEditMode}>
|
icon={<CancelIcon />}
|
||||||
|
>
|
||||||
{I18n.t('common.buttons.cancel')}
|
{I18n.t('common.buttons.cancel')}
|
||||||
</a>
|
</ActionLink>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const BoardForm = ({
|
|||||||
{...register('name', { required: true })}
|
{...register('name', { required: true })}
|
||||||
placeholder={I18n.t('site_settings.boards.form.name')}
|
placeholder={I18n.t('site_settings.boards.form.name')}
|
||||||
autoFocus={mode === 'update'}
|
autoFocus={mode === 'update'}
|
||||||
|
autoComplete={'off'}
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import PostStatusLabel from "../../common/PostStatusLabel";
|
|||||||
import DragZone from '../../common/DragZone';
|
import DragZone from '../../common/DragZone';
|
||||||
import Separator from '../../common/Separator';
|
import Separator from '../../common/Separator';
|
||||||
import PostStatusForm from './PostStatusForm';
|
import PostStatusForm from './PostStatusForm';
|
||||||
|
import ActionLink from '../../common/ActionLink';
|
||||||
|
import { CancelIcon, DeleteIcon, EditIcon } from '../../common/Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -74,13 +76,16 @@ class PostStatusEditable extends React.Component<Props, State> {
|
|||||||
<PostStatusLabel name={name} color={color} />
|
<PostStatusLabel name={name} color={color} />
|
||||||
|
|
||||||
<div className="postStatusEditableActions">
|
<div className="postStatusEditableActions">
|
||||||
<a onClick={this.toggleEditMode}>{I18n.t('common.buttons.edit')}</a>
|
<ActionLink onClick={this.toggleEditMode} icon={<EditIcon />}>
|
||||||
|
{I18n.t('common.buttons.edit')}
|
||||||
|
</ActionLink>
|
||||||
|
|
||||||
<Separator />
|
<ActionLink
|
||||||
|
onClick={() => confirm(I18n.t('common.confirmation')) && handleDelete(id)}
|
||||||
<a onClick={() => confirm(I18n.t('common.confirmation')) && handleDelete(id)}>
|
icon={<DeleteIcon />}
|
||||||
|
>
|
||||||
{I18n.t('common.buttons.delete')}
|
{I18n.t('common.buttons.delete')}
|
||||||
</a>
|
</ActionLink>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
@@ -93,11 +98,12 @@ class PostStatusEditable extends React.Component<Props, State> {
|
|||||||
handleUpdate={this.handleUpdate}
|
handleUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
<ActionLink
|
||||||
className="postStatusFormCancelButton"
|
onClick={this.toggleEditMode}
|
||||||
onClick={this.toggleEditMode}>
|
icon={<CancelIcon />}
|
||||||
|
>
|
||||||
{I18n.t('common.buttons.cancel')}
|
{I18n.t('common.buttons.cancel')}
|
||||||
</a>
|
</ActionLink>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const PostStatusForm = ({
|
|||||||
{...register('name', { required: true })}
|
{...register('name', { required: true })}
|
||||||
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
||||||
autoFocus={mode === 'update'}
|
autoFocus={mode === 'update'}
|
||||||
|
autoComplete={'off'}
|
||||||
className="formControl"
|
className="formControl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import IUser, { UserRoles, USER_ROLE_ADMIN, USER_ROLE_USER, USER_STATUS_ACTIVE,
|
|||||||
import Separator from "../../common/Separator";
|
import Separator from "../../common/Separator";
|
||||||
import UserForm from "./UserForm";
|
import UserForm from "./UserForm";
|
||||||
import { MutedText } from "../../common/CustomTexts";
|
import { MutedText } from "../../common/CustomTexts";
|
||||||
|
import { BlockIcon, CancelIcon, EditIcon, UnblockIcon } from "../../common/Icons";
|
||||||
|
import ActionLink from "../../common/ActionLink";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
@@ -119,18 +121,18 @@ class UserEditable extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="userEditableActions">
|
<div className="userEditableActions">
|
||||||
<a
|
<ActionLink
|
||||||
onClick={() => editEnabled && this.toggleEditMode()}
|
onClick={() => editEnabled && this.toggleEditMode()}
|
||||||
className={editEnabled ? '' : 'actionDisabled'}
|
icon={<EditIcon />}
|
||||||
|
disabled={!editEnabled}
|
||||||
>
|
>
|
||||||
{ I18n.t('common.buttons.edit') }
|
{ I18n.t('common.buttons.edit') }
|
||||||
</a>
|
</ActionLink>
|
||||||
|
|
||||||
<Separator />
|
<ActionLink
|
||||||
|
|
||||||
<a
|
|
||||||
onClick={() => blockEnabled && this._handleUpdateUserStatus()}
|
onClick={() => blockEnabled && this._handleUpdateUserStatus()}
|
||||||
className={blockEnabled ? '' : 'actionDisabled'}
|
icon={user.status !== USER_STATUS_BLOCKED ? <BlockIcon /> : <UnblockIcon />}
|
||||||
|
disabled={!blockEnabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
user.status !== USER_STATUS_BLOCKED ?
|
user.status !== USER_STATUS_BLOCKED ?
|
||||||
@@ -138,15 +140,15 @@ class UserEditable extends React.Component<Props, State> {
|
|||||||
:
|
:
|
||||||
I18n.t('site_settings.users.unblock')
|
I18n.t('site_settings.users.unblock')
|
||||||
}
|
}
|
||||||
</a>
|
</ActionLink>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
<>
|
<>
|
||||||
<UserForm user={user} updateUserRole={this._handleUpdateUserRole} />
|
<UserForm user={user} updateUserRole={this._handleUpdateUserRole} />
|
||||||
<a onClick={this.toggleEditMode} className="userEditCancelButton">
|
<ActionLink onClick={this.toggleEditMode} icon={<CancelIcon />}>
|
||||||
{ I18n.t('common.buttons.cancel') }
|
{I18n.t('common.buttons.cancel')}
|
||||||
</a>
|
</ActionLink>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
26
app/javascript/components/common/ActionLink.tsx
Normal file
26
app/javascript/components/common/ActionLink.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClick: React.MouseEventHandler<HTMLAnchorElement>;
|
||||||
|
icon?: React.ReactElement;
|
||||||
|
disabled?: boolean;
|
||||||
|
customClass?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionLink = ({
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
disabled = false,
|
||||||
|
customClass,
|
||||||
|
children,
|
||||||
|
}: Props) => (
|
||||||
|
<a
|
||||||
|
onClick={onClick}
|
||||||
|
className={`actionLink${disabled ? ' actionLinkDisabled' : ''}${customClass ? ' ' + customClass : ''}`}
|
||||||
|
>
|
||||||
|
{icon}{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ActionLink;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import I18n from 'i18n-js';
|
import I18n from 'i18n-js';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import ActionLink from './ActionLink';
|
||||||
|
import { CopyIcon, DoneIcon } from './Icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -20,7 +22,7 @@ const CopyToClipboardButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
ready ?
|
ready ?
|
||||||
<a
|
<ActionLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
@@ -32,11 +34,14 @@ const CopyToClipboardButton = ({
|
|||||||
alertError();
|
alertError();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
icon={<CopyIcon />}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</ActionLink>
|
||||||
:
|
:
|
||||||
<span>{copiedLabel}</span>
|
<span style={{display: 'flex', marginRight: 12}}>
|
||||||
|
{copiedLabel}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { MdDragIndicator } from 'react-icons/md';
|
||||||
|
|
||||||
const DragZone = ({dndProvided, isDragDisabled, color = 'black'}) => (
|
interface Props {
|
||||||
|
dndProvided: any;
|
||||||
|
isDragDisabled: boolean;
|
||||||
|
color?: 'black' | 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragZone = ({
|
||||||
|
dndProvided,
|
||||||
|
isDragDisabled,
|
||||||
|
color = 'black',
|
||||||
|
}: Props) => (
|
||||||
<span
|
<span
|
||||||
className={`drag-zone${isDragDisabled ? ' drag-zone-disabled' : ''}`}
|
className={`drag-zone${isDragDisabled ? ' drag-zone-disabled' : ''}`}
|
||||||
{...dndProvided.dragHandleProps}
|
{...dndProvided.dragHandleProps}
|
||||||
>
|
>
|
||||||
<span className={`drag-icon${color === 'white' ? ' drag-icon-white' : ''}`}></span>
|
<MdDragIndicator color={color} size={18} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
28
app/javascript/components/common/Icons.tsx
Normal file
28
app/javascript/components/common/Icons.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { BsReply } from 'react-icons/bs';
|
||||||
|
import { FiEdit, FiDelete } from 'react-icons/fi';
|
||||||
|
import { ImCancelCircle } from 'react-icons/im';
|
||||||
|
import { TbLock, TbLockOpen } from 'react-icons/tb';
|
||||||
|
import { MdContentCopy, MdDone, MdOutlineArrowBack } from 'react-icons/md';
|
||||||
|
import { GrTest } from 'react-icons/gr';
|
||||||
|
|
||||||
|
export const EditIcon = () => <FiEdit />;
|
||||||
|
|
||||||
|
export const DeleteIcon = () => <FiDelete />;
|
||||||
|
|
||||||
|
export const CancelIcon = () => <ImCancelCircle />;
|
||||||
|
|
||||||
|
export const BlockIcon = () => <TbLock />;
|
||||||
|
|
||||||
|
export const UnblockIcon = () => <TbLockOpen />;
|
||||||
|
|
||||||
|
export const CopyIcon = () => <MdContentCopy />;
|
||||||
|
|
||||||
|
export const TestIcon = () => <GrTest />;
|
||||||
|
|
||||||
|
export const DoneIcon = () => <MdDone />;
|
||||||
|
|
||||||
|
export const BackIcon = () => <MdOutlineArrowBack />;
|
||||||
|
|
||||||
|
export const ReplyIcon = () => <BsReply />;
|
||||||
@@ -187,4 +187,25 @@
|
|||||||
.link {
|
.link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover { text-decoration: underline; }
|
&:hover { text-decoration: underline; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionLink {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
|
||||||
|
&:hover { text-decoration: underline !important; }
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: 4px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.actionLinkDisabled {
|
||||||
|
color: $astuto-grey !important;
|
||||||
|
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
.commentsContainer {
|
.commentsContainer {
|
||||||
@extend .my-3;
|
@extend .my-3;
|
||||||
|
|
||||||
a.commentLink {
|
|
||||||
color: $primary-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentForm {
|
.commentForm {
|
||||||
@extend
|
@extend
|
||||||
.form-control,
|
.form-control,
|
||||||
@@ -41,7 +32,7 @@
|
|||||||
.justify-content-between,
|
.justify-content-between,
|
||||||
.mt-3;
|
.mt-3;
|
||||||
|
|
||||||
margin-left: 48px;
|
margin-left: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currentUserAvatar {
|
.currentUserAvatar {
|
||||||
@@ -71,6 +62,8 @@
|
|||||||
.d-flex,
|
.d-flex,
|
||||||
.justify-content-between;
|
.justify-content-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editCommentFormActions { @extend .d-flex; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentsTitle {
|
.commentsTitle {
|
||||||
@@ -113,7 +106,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.commentFooter {
|
.commentFooter {
|
||||||
font-size: 14px;
|
@extend .d-flex;
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,12 +141,7 @@
|
|||||||
.postAuthorAvatar { @extend .gravatar; }
|
.postAuthorAvatar { @extend .gravatar; }
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.postFooterActions { @extend .d-flex; }
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.postEditForm {
|
.postEditForm {
|
||||||
@@ -155,14 +150,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.postEditFormButtons {
|
.postEditFormButtons {
|
||||||
text-align: right;
|
@extend .d-flex, .justify-content-end;
|
||||||
|
|
||||||
a {
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
.columnTitle {
|
.columnTitle {
|
||||||
color: white;
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oAuthActions {
|
.oAuthActions {
|
||||||
align-self: center;
|
@extend .d-flex;
|
||||||
|
|
||||||
a {
|
align-self: center;
|
||||||
@extend .link;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,12 +44,8 @@
|
|||||||
|
|
||||||
.authenticationFormPage {
|
.authenticationFormPage {
|
||||||
a.backButton {
|
a.backButton {
|
||||||
@extend .mb-2;
|
@extend .mb-2, .align-self-start;
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.link {
|
|
||||||
@extend .link;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,6 @@
|
|||||||
|
|
||||||
column-gap: 32px;
|
column-gap: 32px;
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover { text-decoration: underline; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardInfo {
|
.boardInfo {
|
||||||
@extend
|
@extend
|
||||||
.d-flex,
|
.d-flex,
|
||||||
@@ -30,8 +24,8 @@
|
|||||||
.boardDescription { @extend .text-center; }
|
.boardDescription { @extend .text-center; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.boardEditableActions, .boardFormCancelButton {
|
.boardEditableActions {
|
||||||
@extend .align-self-center;
|
@extend .d-flex, .align-self-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,7 @@
|
|||||||
.justify-content-between,
|
.justify-content-between,
|
||||||
.p-3;
|
.p-3;
|
||||||
|
|
||||||
a {
|
.postStatusEditableActions { @extend .d-flex; }
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover { text-decoration: underline; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.postStatusFormCancelButton {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
|
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
.titleText { @extend .align-self-center; }
|
.titleText { @extend .align-self-center; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ul.usersList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.userEditableActions {
|
.userEditableActions {
|
||||||
@extend .align-self-center;
|
@extend .d-flex, .align-self-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userForm {
|
.userForm {
|
||||||
@@ -41,24 +41,8 @@ ul.usersList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.updateUserButton { @extend .align-self-center; }
|
|
||||||
|
|
||||||
.userEditCancelButton { @extend .align-self-center; }
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover { text-decoration: underline; }
|
|
||||||
}
|
|
||||||
|
|
||||||
a.actionDisabled {
|
|
||||||
@extend .mutedText;
|
|
||||||
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.updateUserButton {
|
.updateUserButton {
|
||||||
|
@extend .align-self-center;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/* Code taken from: https://gist.github.com/JakeSidSmith/83c324dbe7e4d91ee8c52525b1d504d9 */
|
|
||||||
/* Thanks JakeSidSmith */
|
|
||||||
|
|
||||||
span.drag-icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.drag-icon,
|
|
||||||
span.drag-icon::before {
|
|
||||||
background-image: radial-gradient(black 40%, transparent 40%);
|
|
||||||
background-size: 4px 4px;
|
|
||||||
background-position: 0 100%;
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.drag-icon.drag-icon-white,
|
|
||||||
span.drag-icon.drag-icon-white::before {
|
|
||||||
background-image: radial-gradient(white 40%, transparent 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
span.drag-icon::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 33%;
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,4 @@
|
|||||||
@import 'components/SiteSettings/PostStatuses';
|
@import 'components/SiteSettings/PostStatuses';
|
||||||
@import 'components/SiteSettings/Roadmap';
|
@import 'components/SiteSettings/Roadmap';
|
||||||
@import 'components/SiteSettings/Users';
|
@import 'components/SiteSettings/Users';
|
||||||
@import 'components/SiteSettings/Authentication';
|
@import 'components/SiteSettings/Authentication';
|
||||||
|
|
||||||
/* Icons */
|
|
||||||
@import 'icons/drag_icon';
|
|
||||||
@@ -40,10 +40,9 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<% if not @o_auths.empty? %>
|
<% if not @o_auths.empty? %>
|
||||||
|
<hr />
|
||||||
<% @o_auths.each do |o_auth| %>
|
<% @o_auths.each do |o_auth| %>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('common.forms.auth.sign_up_with', { o_auth: o_auth.name }),
|
<%= link_to t('common.forms.auth.sign_up_with', { o_auth: o_auth.name }),
|
||||||
|
|||||||
@@ -29,10 +29,9 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<% if not @o_auths.empty? %>
|
<% if not @o_auths.empty? %>
|
||||||
|
<hr />
|
||||||
<% @o_auths.each do |o_auth| %>
|
<% @o_auths.each do |o_auth| %>
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('common.forms.auth.log_in_with', { o_auth: o_auth.name }),
|
<%= link_to t('common.forms.auth.log_in_with', { o_auth: o_auth.name }),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ en:
|
|||||||
update: 'Save'
|
update: 'Save'
|
||||||
confirm: 'Confirm'
|
confirm: 'Confirm'
|
||||||
back: 'Back'
|
back: 'Back'
|
||||||
|
test: 'Test'
|
||||||
datetime:
|
datetime:
|
||||||
now: 'just now'
|
now: 'just now'
|
||||||
minutes:
|
minutes:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
"react-gravatar": "^2.6.3",
|
"react-gravatar": "^2.6.3",
|
||||||
"react-hook-form": "7.33.1",
|
"react-hook-form": "7.33.1",
|
||||||
|
"react-icons": "4.4.0",
|
||||||
"react-infinite-scroller": "^1.2.4",
|
"react-infinite-scroller": "^1.2.4",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"react-redux": "^7.1.1",
|
"react-redux": "^7.1.1",
|
||||||
|
|||||||
@@ -6388,6 +6388,11 @@ react-hook-form@7.33.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0"
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0"
|
||||||
integrity sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==
|
integrity sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg==
|
||||||
|
|
||||||
|
react-icons@4.4.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.4.0.tgz#a13a8a20c254854e1ec9aecef28a95cdf24ef703"
|
||||||
|
integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==
|
||||||
|
|
||||||
react-infinite-scroller@^1.2.4:
|
react-infinite-scroller@^1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9"
|
resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9"
|
||||||
|
|||||||
Reference in New Issue
Block a user