[CORL-156] Manage user suspension status (#2419)

* wire up suspension modal

* show user suspended callout on stream

* prevent comment actions for suspended users

* set default to 3 hour suspension

* add message field to suspension

* allow custom message for suspension

* show suspend success modal

* fix type errors

* remove unused code

* update styles

* fix fixture for streams

* add suspension ui tests

* fix types

* remove warnings?

* remove snapshot

* fix merge conflicts

* allow custom email message when banning users

* fix typo

* correct message type

* use final-form in suspend modal

* refactor suspend modal to use final-form

* refactor ban modal to use final-form

* refactor userStatusChangeContainer to use useCallback

* feat: improve translated form

* fix: addressed issue caused by i18n refactor

* update getMessage to accept arguments, remove format method

* translate suspend info

* change hour format

* make message a mandatory input for suspend and ban user

* fix types in user table

* Revert "fix types in user table"

This reverts commit d396e90b88bb1bd354c5cdbdd72b6d8f1ab72929.

* fix types for user table

* fix: small review tweaks
This commit is contained in:
Tessa Thornton
2019-08-02 21:16:21 +00:00
committed by Wyatt Johnson
parent 4e548e8fbf
commit 5df2de6afc
42 changed files with 1511 additions and 276 deletions
@@ -1,6 +1,7 @@
import { Localized } from "fluent-react/compat";
import React, { ChangeEvent, Component } from "react";
import { UNIT } from "coral-framework/lib/i18n";
import { Flex, Option, SelectField, TextField } from "coral-ui/components";
import styles from "./DurationField.css";
@@ -9,22 +10,16 @@ import styles from "./DurationField.css";
* DURATION_UNIT are units that can be used in the
* DurationField components.
*/
export enum DURATION_UNIT {
SECONDS = 1,
MINUTES = 60,
HOURS = 3600,
DAYS = 86400,
WEEKS = 604800,
}
export const DURATION_UNIT = UNIT;
type UnitElementCallback = (
currentValue: DURATION_UNIT,
currentValue: UNIT,
unitValue: string
) => React.ReactElement<any>;
// This is used to render the Option elements to inlcude in the select field.
const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
[DURATION_UNIT.SECONDS]: (currentValue, unitValue) => (
const unitElementMap: Record<UNIT, UnitElementCallback> = {
[UNIT.SECONDS]: (currentValue, unitValue) => (
<Localized
id="framework-durationField-seconds"
$value={currentValue}
@@ -33,7 +28,7 @@ const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
<Option value={unitValue}>Seconds</Option>
</Localized>
),
[DURATION_UNIT.MINUTES]: (currentValue, unitValue) => (
[UNIT.MINUTES]: (currentValue, unitValue) => (
<Localized
id="framework-durationField-minutes"
$value={currentValue}
@@ -42,7 +37,7 @@ const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
<Option value={unitValue}>Minutes</Option>
</Localized>
),
[DURATION_UNIT.HOURS]: (currentValue, unitValue) => (
[UNIT.HOURS]: (currentValue, unitValue) => (
<Localized
id="framework-durationField-hours"
$value={currentValue}
@@ -51,7 +46,7 @@ const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
<Option value={unitValue}>Hours</Option>
</Localized>
),
[DURATION_UNIT.DAYS]: (currentValue, unitValue) => (
[UNIT.DAYS]: (currentValue, unitValue) => (
<Localized
id="framework-durationField-days"
$value={currentValue}
@@ -60,7 +55,7 @@ const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
<Option value={unitValue}>Days</Option>
</Localized>
),
[DURATION_UNIT.WEEKS]: (currentValue, unitValue) => (
[UNIT.WEEKS]: (currentValue, unitValue) => (
<Localized
id="framework-durationField-weeks"
$value={currentValue}
@@ -77,16 +72,16 @@ interface Props {
disabled: boolean;
onChange: (v: string) => void;
/** Specifiy units to include */
units?: ReadonlyArray<DURATION_UNIT>;
units?: ReadonlyArray<UNIT>;
}
interface State {
/** Current value */
value: string;
/** Current unit */
unit?: DURATION_UNIT;
unit?: UNIT;
/** All available units */
units: ReadonlyArray<DURATION_UNIT>;
units: ReadonlyArray<UNIT>;
/**
* Element callbacks to generate the rendered
* Option element for the select field
@@ -100,11 +95,7 @@ interface State {
* @param units The units that we use.
* @param unit The current value if any otherwise the best matching unit will be used.
*/
function valueToState(
value: string,
units: ReadonlyArray<DURATION_UNIT>,
unit?: DURATION_UNIT
) {
function valueToState(value: string, units: ReadonlyArray<UNIT>, unit?: UNIT) {
const parsed = parseInt(value, 10);
// If value was a valid number..
@@ -147,7 +138,7 @@ function stateToValue(state: State) {
*/
class DurationField extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
units: [DURATION_UNIT.HOURS, DURATION_UNIT.DAYS, DURATION_UNIT.WEEKS],
units: [UNIT.HOURS, UNIT.DAYS, UNIT.WEEKS],
};
public state: State = valueToState(this.props.value, this.props.units!);
@@ -0,0 +1,18 @@
import { FluentBundle } from "fluent/compat";
export default function format(
bundles: FluentBundle[],
key: string,
args?: object
): string {
const res = bundles.reduce((val, bundle) => {
const message = bundle.getMessage(key);
const got = bundle.format(message, args);
return val || got;
}, "");
if (res && Array.isArray(res)) {
return res.join("");
}
return res || "";
}
@@ -1,17 +1,21 @@
import { FluentBundle } from "fluent/compat";
export default function getMessage(
export default function getMessage<T extends {}>(
bundles: FluentBundle[],
key: string,
defaultTo = ""
defaultTo: string,
args?: T
): string {
const res = bundles.reduce((val, bundle) => {
const got = bundle.getMessage(key);
if (!got && process.env.NODE_ENV !== "production") {
const message = bundle.getMessage(key);
if (!message && process.env.NODE_ENV !== "production") {
// tslint:disable-next-line:no-console
console.warn(`Translation ${key} was not found for ${bundle.locales}`);
}
return val || got;
if (!args) {
return val || message;
}
return val || bundle.format(message, args);
}, "");
if (res && Array.isArray(res)) {
return res.join("");
@@ -3,3 +3,4 @@ export { default as negotiateLanguages } from "./negotiateLanguages";
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
export { default as getMessage } from "./getMessage";
export { default as withGetMessage, GetMessage } from "./withGetMessage";
export { default as reduceSeconds, UNIT, ScaledUnit } from "./reduceSeconds";
@@ -0,0 +1,55 @@
/**
* UNIT are units that can be used in the
* DurationField components.
*/
export enum UNIT {
SECONDS = 1,
MINUTES = 60,
HOURS = 3600,
DAYS = 86400,
WEEKS = 604800,
}
export const UNIT_MAP = {
[UNIT.SECONDS]: "second",
[UNIT.MINUTES]: "minute",
[UNIT.HOURS]: "hour",
[UNIT.DAYS]: "day",
[UNIT.WEEKS]: "week",
};
export const DEFAULT_UNITS = [
UNIT.WEEKS,
UNIT.DAYS,
UNIT.HOURS,
UNIT.MINUTES,
UNIT.SECONDS,
];
type ValueOf<T> = T[keyof T];
export interface ScaledUnit {
original: number;
value: string;
unit: ValueOf<typeof UNIT_MAP>;
scaled: number;
}
export default function reduceSeconds(
value: number,
units: UNIT[] = DEFAULT_UNITS
): ScaledUnit {
// Find the largest match for the smallest number.
const unit: keyof typeof UNIT_MAP =
units.find(compare => value >= compare) || UNIT.SECONDS;
// Scale the value to the unit.
const scaled = Math.round((value / unit) * 100) / 100;
return {
original: value,
value: value.toString(),
scaled,
unit: UNIT_MAP[unit],
};
}
@@ -4,7 +4,7 @@ import { DefaultingInferableComponentEnhancer, hoistStatics } from "recompose";
import { CoralContext, withContext } from "../bootstrap";
import getMessage from "./getMessage";
export type GetMessage = (id: string, defaultTo?: string) => string;
export type GetMessage = <T>(id: string, defaultTo: string, args?: T) => string;
interface InjectedProps {
getMessage: GetMessage;
@@ -35,8 +35,12 @@ const withGetMessage: DefaultingInferableComponentEnhancer<
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
class WithGetMessage extends React.Component<Props> {
private getMessage = (id: string, defaultTo?: string) => {
return getMessage(this.props.localeBundles, id, defaultTo);
private getMessage = <U extends {}>(
id: string,
defaultTo: string,
args?: U
): string => {
return getMessage(this.props.localeBundles, id, defaultTo, args);
};
public render() {
const { localeBundles: _, ...rest } = this.props;