ISSUE-73: UI support for DeleteMe request

Closes #73

* Add the API post request deleteMe
* Add a trigger button
* Add an utils/email module with the template and window opener
* Add a dropdown component
* Add a button component
* Hide the user-id text and a logout bout in the dropdown
* Add a noop empty function to pass a default handler callback to components
This commit is contained in:
Eldar Amantay
2018-07-17 23:21:11 +04:00
committed by Aleksei Gurianov
parent 592def143a
commit 1d267f5db6
27 changed files with 335 additions and 20 deletions
+7
View File
@@ -69,6 +69,13 @@ export const getUser = () =>
withCredentials: true,
});
/* GDPR */
export const deleteMe = () =>
fetcher.post({
url: `/deleteme?site=${siteId}`,
});
/* admin */
export const pinComment = ({ id, url }) =>
fetcher.put({
@@ -1,8 +1,6 @@
.auth-panel__column {
&:nth-child(1) {
overflow: hidden;
font-weight: 700;
text-overflow: ellipsis;
}
&:nth-child(2) {
@@ -0,0 +1,3 @@
.auth-panel__sign-out {
margin-left: 5px;
}
@@ -48,7 +48,7 @@ describe('<AuthPanel />', () => {
const userInfo = authPanelColumn[0];
expect(userInfo.textContent).toEqual(expect.stringContaining('You signed in as John. Sign out?'));
expect(userInfo.textContent).toEqual(expect.stringContaining('You signed in as John'));
});
});
describe('For admin user', () => {
@@ -0,0 +1,8 @@
/** @jsx h */
import { h } from 'preact';
export default ({ id }) => (
<div className="auth-panel__user-id" title={id}>
{id}
</div>
);
@@ -1,3 +1,5 @@
.auth-panel__user-id {
overflow: hidden;
text-overflow: ellipsis;
color: #888;
}
@@ -1,3 +0,0 @@
.auth-panel__username {
cursor: default;
}
+23 -12
View File
@@ -1,14 +1,17 @@
/** @jsx h */
import { h, Component } from 'preact';
import UserId from './__user-id/auth-panel__user-id';
import Dropdown, { DropdownItem } from 'components/dropdown';
import Button from 'components/button';
import { PROVIDER_NAMES } from 'common/constants';
import { requestDeletion } from 'utils/email';
import { getHandleClickProps } from 'common/accessibility';
export default class AuthPanel extends Component {
constructor(props) {
super(props);
this.toggleUserId = this.toggleUserId.bind(this);
this.toggleBlockedVisibility = this.toggleBlockedVisibility.bind(this);
this.toggleCommentsAvailability = this.toggleCommentsAvailability.bind(this);
this.onSortChange = this.onSortChange.bind(this);
@@ -20,10 +23,6 @@ export default class AuthPanel extends Component {
}
}
toggleUserId() {
this.setState({ isUserIdVisible: !this.state.isUserIdVisible });
}
toggleBlockedVisibility() {
if (!this.state.isBlockedVisible) {
if (this.props.onBlockedUsersShow) this.props.onBlockedUsersShow();
@@ -44,7 +43,12 @@ export default class AuthPanel extends Component {
}
}
render(props, { isUserIdVisible, isBlockedVisible }) {
getUserTitle() {
const { user } = this.props;
return <span className="auth-panel__username">{user.name}</span>;
}
render(props, { isBlockedVisible }) {
const { user, providers = [], sort, isCommentsDisabled } = props;
const sortArray = getSortArray(sort);
@@ -55,13 +59,20 @@ export default class AuthPanel extends Component {
{loggedIn && (
<div className="auth-panel__column">
You signed in as{' '}
<strong {...getHandleClickProps(this.toggleUserId)} className="auth-panel__username">
{user.name}
</strong>
{isUserIdVisible && <span className="auth-panel__user-id"> ({user.id})</span>}.{' '}
<span {...getHandleClickProps(props.onSignOut)} className="auth-panel__pseudo-link" role="link">
<Dropdown title={user.name}>
<DropdownItem separator>
<UserId id={user.id} />
</DropdownItem>
<DropdownItem>
<Button mods={{ kind: 'link' }} onClick={requestDeletion}>
Request my data removal
</Button>
</DropdownItem>
</Dropdown>{' '}
<Button className="auth-panel__sign-out" mods={{ kind: 'link' }} onClick={props.onSignOut}>
Sign out?
</span>
</Button>
</div>
)}
+2 -1
View File
@@ -7,7 +7,8 @@ require('./__pseudo-link/auth-panel__pseudo-link.scss');
require('./__select/auth-panel__select.scss');
require('./__select-label/auth-panel__select-label.scss');
require('./__sort/auth-panel__sort.scss');
require('./__username/auth-panel__username.scss');
require('./__user-id/auth-panel__user-id.scss');
require('./__sign-out/auth-panel__sign-out.scss');
require('./_logged-in/auth-panel_logged-in.scss');
@@ -0,0 +1,3 @@
.button_focused:not(.button_clicked) {
outline-width: 5px;
}
@@ -0,0 +1,19 @@
.button_kind_text {
border: none;
background: transparent;
padding: 0;
}
.button_kind_link {
border: none;
background: transparent;
padding: 0;
font-weight: 700;
white-space: nowrap;
color: #0aa;
&:hover {
color: #06c5c5;
}
}
+69
View File
@@ -0,0 +1,69 @@
/** @jsx h */
import { Component, h } from 'preact';
import noop from '../../utils/noop';
export default class Button extends Component {
constructor(props) {
super(props);
this.state = {
isClicked: false,
isFocused: false,
};
this.onMouseDown = this.onMouseDown.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
}
onMouseDown() {
this.setState({
isClicked: true,
});
}
onClick(e) {
this.props.onClick(e);
}
onBlur(e) {
this.setState({
isClicked: false,
isFocused: false,
});
this.props.onBlur(e);
}
onFocus(e) {
this.setState({
isFocused: true,
});
this.props.onFocus(e);
}
render(props, state) {
const { children, mix, mods, ...rest } = props;
const { isClicked, isFocused } = state;
return (
<button
{...rest}
className={b('button', { mix, mods }, { clicked: isClicked, focused: isFocused })}
onMouseDown={this.onMouseDown}
onBlur={this.onBlur}
onFocus={this.onFocus}
>
{children}
</button>
);
}
}
Button.defaultProps = {
type: 'button',
onClick: noop,
onBlur: noop,
onFocus: noop,
};
+6
View File
@@ -0,0 +1,6 @@
.button {
cursor: pointer;
outline-width: 0;
font-size: inherit;
font-family: inherit;
}
+5
View File
@@ -0,0 +1,5 @@
export { default } from './button';
require('./button.scss');
require('./_kind/button_kind.scss');
require('./_focused/button_focused.scss');
@@ -0,0 +1,14 @@
.dropdown__content {
position: absolute;
z-index: 20;
outline-width: 0;
display: none;
top: 100%;
left: 50%;
transform: translate(-50%, 5px);
width: 170px;
background-color: #fff;
border: 2px solid #259c9a;
border-radius: 3px;
padding: 0 0 5px;
}
@@ -0,0 +1,8 @@
/** @jsx h */
import { h } from 'preact';
export default function DropdownItem(props) {
const { children, separator = false, mix, mods } = props;
return <div className={b('dropdown__item', { mix, mods }, { separator })}>{children}</div>;
}
@@ -0,0 +1,14 @@
.dropdown__item {
a, button {
display: block;
width: 100%;
text-align: left;
padding: 5px 15px;
}
&_separator {
border-bottom: 1px solid #259c9a;
margin-bottom: 5px;
padding: 5px 15px;
}
}
@@ -0,0 +1,3 @@
.dropdown__items {
padding: 5px 0;
}
@@ -0,0 +1,12 @@
.dropdown__title {
border: none;
background: none;
font-weight: bold;
padding: 0;
margin: 0;
&::after {
content: '\25BE';
margin-left: 2px;
}
}
@@ -0,0 +1,5 @@
.dropdown_active {
.dropdown__content {
display: block;
}
}
+76
View File
@@ -0,0 +1,76 @@
/** @jsx h */
import { Component, h } from 'preact';
import Button from 'components/button';
export default class Dropdown extends Component {
constructor(props) {
super(props);
this.state = {
isActive: props.isActive || false,
};
this.onTitleClick = this.onTitleClick.bind(this);
this.onOutsideClick = this.onOutsideClick.bind(this);
}
onTitleClick() {
this.setState({
isActive: !this.state.isActive,
});
if (this.props.onTitleClick) {
this.props.onTitleClick();
}
}
onOutsideClick(e) {
if (!this.rootNode.contains(e.target)) {
if (this.state.isActive) {
this.setState({
isActive: false,
});
}
}
}
componentDidMount() {
document.addEventListener('click', this.onOutsideClick);
if (parent) {
parent.document.addEventListener('click', this.onOutsideClick);
}
}
componentWillUnmount() {
document.removeEventListener('click', this.onOutsideClick);
if (parent) {
parent.document.removeEventListener('click', this.onOutsideClick);
}
}
render(props, { isActive }) {
const { title, heading, children, mix, mods } = props;
return (
<div className={b('dropdown', { mix, mods }, { active: isActive })} ref={r => (this.rootNode = r)}>
<Button
aria-haspopup="listbox"
aria-expanded={isActive && 'true'}
mix="dropdown__title"
type="button"
onClick={this.onTitleClick}
>
{title}
</Button>
<div className="dropdown__content" tabindex="-1" role="listbox">
{heading && <div className="dropdown__heading">{heading}</div>}
<div className="dropdown__items">{children}</div>
</div>
</div>
);
}
}
@@ -0,0 +1,4 @@
.dropdown {
display: inline-block;
position: relative;
}
+10
View File
@@ -0,0 +1,10 @@
export { default } from './dropdown';
export { default as DropdownItem } from './__item/dropdown__item';
require('./dropdown.scss');
require('./_active/dropdown_active.scss');
require('./__item/dropdown__item.scss');
require('./__items/dropdown__items.scss');
require('./__title/dropdown__title.scss');
require('./__content/dropdown__content.scss');
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+35
View File
@@ -0,0 +1,35 @@
import store from '../common/store';
import { siteId } from '../common/settings';
import { deleteMe } from '../common/api';
// The right line breaks code in the body of inline email
// should be not just %0A, but %0D%0A
// see: https://www.ietf.org/rfc/rfc2368.txt
const LINE_BREAK_CODE = '%0D%0A';
export function getDeleteInformationMessage(userId, siteId, link) {
const subject = encodeURIComponent("Request to delete user's information");
const message = encodeURIComponent(`Request to delete all information about ${userId} from remark42 on ${siteId}
[you can provide the reason for removal request, optional]
=== DO NOT REMOVE THE TEXT BELOW THIS LINE ===
site: ${siteId}
user: ${userId}
link: ${link}
`).replace('%0A', LINE_BREAK_CODE);
return {
subject,
message,
};
}
export function requestDeletion() {
deleteMe().then(data => {
const email = store.get('config').admin_email;
const { subject, message } = getDeleteInformationMessage(data.user_id, siteId, data.link);
window.open(`mailto:${email}?subject=${subject}&body=${message}`);
});
}
+1
View File
@@ -0,0 +1 @@
export default function noop() {}
+4 -1
View File
@@ -83,7 +83,10 @@
],
"testMatch": [
"<rootDir>/**/*.test.js"
]
],
"moduleNameMapper": {
"\\.scss$": "<rootDir>/app/testUtils/mockStyles.js"
}
},
"engines": {
"node": ">=8"