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:
committed by
Aleksei Gurianov
parent
592def143a
commit
1d267f5db6
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.button {
|
||||
cursor: pointer;
|
||||
outline-width: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function noop() {}
|
||||
+4
-1
@@ -83,7 +83,10 @@
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/**/*.test.js"
|
||||
]
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.scss$": "<rootDir>/app/testUtils/mockStyles.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
||||
Reference in New Issue
Block a user