mirror of
https://github.com/wassname/talk.git
synced 2026-07-18 12:40:13 +08:00
[CORL-155] User Suspending and Banning (#2247)
* feat: suspending, banning, now propogation * feat: adapting to `now` * feat: support auth for suspension/banned * feat: added trace-id to requests * feat: new mutation api with hooks support * feat: added user status filtering, current field * feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3 * fix: typo * fix: tests * chore: rename banned status to ban status * test: feature test + lots of test helper improvements e.g. types * fix: add translation to ban user modal * fix: translation * fix: test
This commit is contained in:
Generated
+9
-9
@@ -2670,9 +2670,9 @@
|
||||
"integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q=="
|
||||
},
|
||||
"@types/luxon": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-0.5.3.tgz",
|
||||
"integrity": "sha512-YiVw0M9q9CeynfRKhZYaX2/aCXlCIBpM4eARlPXdv+XVoGVb5iPFaZIlKiMUJ8eWKOhlqi8U6GvOAn8yhR4//Q==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.12.0.tgz",
|
||||
"integrity": "sha512-+UzPmwHSEEyv7aGlNkVpuFxp/BirXgl8NnPGCtmyx2KXIzAapoW3IqSVk87/Z3PUk8vEL8Pe1HXEMJbNBOQgtg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/marked": {
|
||||
@@ -18199,9 +18199,9 @@
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.3.1.tgz",
|
||||
"integrity": "sha512-GnX4PVEfDcTZT3xDQhm2jvEyZTxfi2b0gOmkJuwrrggA1b46e8ZshmPYePA6gdAT6mvvOxYnGFfb3iE9ZsWn9g=="
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.12.0.tgz",
|
||||
"integrity": "sha512-enPnPIHd5ZnZT0vpj9Xv8aq4j0yueAkhnh4xUKUHpqlgSm1r/8s6xTMjfyp2ugOWP7zivqJqgVTkW+rpHed61w=="
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
@@ -28190,9 +28190,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz",
|
||||
"integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==",
|
||||
"version": "3.3.4000",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz",
|
||||
"integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript-snapshots-plugin": {
|
||||
|
||||
+2
-2
@@ -156,7 +156,7 @@
|
||||
"@types/linkify-it": "^2.0.4",
|
||||
"@types/linkifyjs": "^2.1.0",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"@types/luxon": "^0.5.3",
|
||||
"@types/luxon": "^1.12.0",
|
||||
"@types/marked": "^0.6.0",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
"@types/mongodb": "^3.1.22",
|
||||
@@ -302,7 +302,7 @@
|
||||
"typed-css-modules": "^0.3.4",
|
||||
"typeface-manuale": "0.0.54",
|
||||
"typeface-source-sans-pro": "0.0.54",
|
||||
"typescript": "3.1.6",
|
||||
"typescript": "3.3.4000",
|
||||
"typescript-snapshots-plugin": "^1.2.0",
|
||||
"wait-for-expect": "^1.1.0",
|
||||
"webpack": "^4.27.1",
|
||||
|
||||
@@ -2,20 +2,17 @@ import { Match, Router, withRouter } from "found";
|
||||
import React from "react";
|
||||
|
||||
import { AuthCheckContainerQueryResponse } from "talk-admin/__generated__/AuthCheckContainerQuery.graphql";
|
||||
import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { SetRedirectPathMutation } from "talk-admin/mutations";
|
||||
import { AbilityType, can } from "talk-admin/permissions";
|
||||
import RestrictedContainer from "talk-admin/views/restricted/containers/RestrictedContainer";
|
||||
import { graphql } from "talk-framework/lib/relay";
|
||||
import { graphql, MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
interface Props {
|
||||
match: Match;
|
||||
router: Router;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
setRedirectPath: MutationProp<typeof SetRedirectPathMutation>;
|
||||
data:
|
||||
| AuthCheckContainerQueryResponse & {
|
||||
route: {
|
||||
@@ -124,6 +121,6 @@ const enhanced = withRouteConfig({
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(withRouter(withSetRedirectPathMutation(AuthCheckContainer)));
|
||||
})(withRouter(withMutation(SetRedirectPathMutation)(AuthCheckContainer)));
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,80 +1,69 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { AcceptCommentMutation as MutationTypes } from "talk-admin/__generated__/AcceptCommentMutation.graphql";
|
||||
import { getQueueConnection } from "talk-admin/helpers";
|
||||
|
||||
export type AcceptCommentInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation AcceptCommentMutation($input: AcceptCommentInput!) {
|
||||
acceptComment(input: $input) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
}
|
||||
moderationQueues {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
reported {
|
||||
count
|
||||
}
|
||||
pending {
|
||||
count
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: AcceptCommentInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
acceptComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
updater: store => {
|
||||
const connections = [
|
||||
getQueueConnection("reported", store),
|
||||
getQueueConnection("pending", store),
|
||||
getQueueConnection("unmoderated", store),
|
||||
getQueueConnection("rejected", store),
|
||||
].filter(c => c);
|
||||
connections.forEach(con =>
|
||||
ConnectionHandler.deleteNode(con, input.commentID)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withAcceptCommentMutation = createMutationContainer(
|
||||
const AcceptCommentMutation = createMutation(
|
||||
"acceptComment",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation AcceptCommentMutation($input: AcceptCommentInput!) {
|
||||
acceptComment(input: $input) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
}
|
||||
moderationQueues {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
reported {
|
||||
count
|
||||
}
|
||||
pending {
|
||||
count
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
acceptComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const connections = [
|
||||
getQueueConnection("reported", store),
|
||||
getQueueConnection("pending", store),
|
||||
getQueueConnection("unmoderated", store),
|
||||
getQueueConnection("rejected", store),
|
||||
].filter(c => c);
|
||||
connections.forEach(con =>
|
||||
ConnectionHandler.deleteNode(con, input.commentID)
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type AcceptCommentMutation = (
|
||||
input: AcceptCommentInput
|
||||
) => MutationResponsePromise<MutationTypes, "acceptComment">;
|
||||
export default AcceptCommentMutation;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { BanUserMutation as MutationTypes } from "talk-admin/__generated__/BanUserMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "talk-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const BanUserMutation = createMutation(
|
||||
"banUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation BanUserMutation($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
ban {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
banUser: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status!.current!.concat([GQLUSER_STATUS.BANNED]),
|
||||
ban: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default BanUserMutation;
|
||||
@@ -1,18 +1,15 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export type ClearAuthErrorMutation = () => Promise<void>;
|
||||
|
||||
export async function commit(environment: Environment, input: undefined) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(null, "authError");
|
||||
});
|
||||
}
|
||||
|
||||
export const withClearAuthErrorMutation = createMutationContainer(
|
||||
const ClearAuthErrorMutation = createMutation(
|
||||
"clearAuthError",
|
||||
commit
|
||||
(environment: Environment) =>
|
||||
commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(null, "authError");
|
||||
})
|
||||
);
|
||||
|
||||
export default ClearAuthErrorMutation;
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CloseStoryMutation as MutationTypes } from "talk-admin/__generated__/CloseStoryMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { CloseStoryMutation as MutationTypes } from "talk-admin/__generated__/CloseStoryMutation.graphql";
|
||||
import { GQLSTORY_STATUS } from "talk-framework/schema";
|
||||
|
||||
export type CloseStoryInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation CloseStoryMutation($input: CloseStoryInput!) {
|
||||
closeStory(input: $input) {
|
||||
story {
|
||||
id
|
||||
status
|
||||
closedAt
|
||||
isClosed
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CloseStoryInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
optimisticResponse: {
|
||||
closeStory: {
|
||||
story: {
|
||||
id: input.id,
|
||||
status: GQLSTORY_STATUS.CLOSED,
|
||||
closedAt: new Date().toISOString(),
|
||||
isClosed: true,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withCloseStoryMutation = createMutationContainer(
|
||||
const CloseStoryMutation = createMutation(
|
||||
"closeStory",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation CloseStoryMutation($input: CloseStoryInput!) {
|
||||
closeStory(input: $input) {
|
||||
story {
|
||||
id
|
||||
status
|
||||
closedAt
|
||||
isClosed
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
optimisticResponse: {
|
||||
closeStory: {
|
||||
story: {
|
||||
id: input.id,
|
||||
status: GQLSTORY_STATUS.CLOSED,
|
||||
closedAt: new Date().toISOString(),
|
||||
isClosed: true,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type CloseStoryMutation = (
|
||||
input: CloseStoryInput
|
||||
) => MutationResponsePromise<MutationTypes, "closeStory">;
|
||||
export default CloseStoryMutation;
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
import { commit as setAccessToken } from "talk-framework/mutations/SetAccessTokenMutation";
|
||||
|
||||
interface CompleteAccountInput {
|
||||
accessToken: string;
|
||||
}
|
||||
export type CompleteAccountMutation = (
|
||||
input: CompleteAccountInput
|
||||
) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: CompleteAccountInput,
|
||||
context: TalkContext
|
||||
) {
|
||||
await setAccessToken(
|
||||
environment,
|
||||
{ accessToken: input.accessToken },
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
export const withCompleteAccountMutation = createMutationContainer(
|
||||
const CompleteAccountMutation = createMutation(
|
||||
"completeAccount",
|
||||
commit
|
||||
async (
|
||||
environment: Environment,
|
||||
input: {
|
||||
accessToken: string;
|
||||
},
|
||||
context: TalkContext
|
||||
) =>
|
||||
await setAccessToken(
|
||||
environment,
|
||||
{ accessToken: input.accessToken },
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
export default CompleteAccountMutation;
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { OpenStoryMutation as MutationTypes } from "talk-admin/__generated__/OpenStoryMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { OpenStoryMutation as MutationTypes } from "talk-admin/__generated__/OpenStoryMutation.graphql";
|
||||
import { GQLSTORY_STATUS } from "talk-framework/schema";
|
||||
|
||||
export type OpenStoryInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation OpenStoryMutation($input: OpenStoryInput!) {
|
||||
openStory(input: $input) {
|
||||
story {
|
||||
id
|
||||
status
|
||||
closedAt
|
||||
isClosed
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: OpenStoryInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
optimisticResponse: {
|
||||
openStory: {
|
||||
story: {
|
||||
id: input.id,
|
||||
status: GQLSTORY_STATUS.OPEN,
|
||||
closedAt: null,
|
||||
isClosed: false,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withOpenStoryMutation = createMutationContainer(
|
||||
const OpenStoryMutation = createMutation(
|
||||
"openStory",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation OpenStoryMutation($input: OpenStoryInput!) {
|
||||
openStory(input: $input) {
|
||||
story {
|
||||
id
|
||||
status
|
||||
closedAt
|
||||
isClosed
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
optimisticResponse: {
|
||||
openStory: {
|
||||
story: {
|
||||
id: input.id,
|
||||
status: GQLSTORY_STATUS.OPEN,
|
||||
closedAt: null,
|
||||
isClosed: false,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type OpenStoryMutation = (
|
||||
input: OpenStoryInput
|
||||
) => MutationResponsePromise<MutationTypes, "openStory">;
|
||||
export default OpenStoryMutation;
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { RegenerateSSOKeyMutation as MutationTypes } from "talk-admin/__generated__/RegenerateSSOKeyMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationResponsePromise,
|
||||
createMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { RegenerateSSOKeyMutation as MutationTypes } from "talk-admin/__generated__/RegenerateSSOKeyMutation.graphql";
|
||||
|
||||
const mutation = graphql`
|
||||
mutation RegenerateSSOKeyMutation($input: RegenerateSSOKeyInput!) {
|
||||
regenerateSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
key
|
||||
keyGeneratedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withRegenerateSSOKeyMutation = createMutationContainer(
|
||||
const RegenerateSSOKeyMutation = createMutation(
|
||||
"regenerateSSOKey",
|
||||
commit
|
||||
(environment: Environment) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RegenerateSSOKeyMutation($input: RegenerateSSOKeyInput!) {
|
||||
regenerateSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
key
|
||||
keyGeneratedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type RegenerateSSOKeyMutation = () => MutationResponsePromise<
|
||||
MutationTypes,
|
||||
"regenerateSSOKey"
|
||||
>;
|
||||
export default RegenerateSSOKeyMutation;
|
||||
|
||||
@@ -1,79 +1,68 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { RejectCommentMutation as MutationTypes } from "talk-admin/__generated__/RejectCommentMutation.graphql";
|
||||
import { getQueueConnection } from "talk-admin/helpers";
|
||||
|
||||
export type RejectCommentInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation RejectCommentMutation($input: RejectCommentInput!) {
|
||||
rejectComment(input: $input) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
}
|
||||
moderationQueues {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
reported {
|
||||
count
|
||||
}
|
||||
pending {
|
||||
count
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: RejectCommentInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
rejectComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: "REJECTED",
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
updater: store => {
|
||||
const connections = [
|
||||
getQueueConnection("reported", store),
|
||||
getQueueConnection("pending", store),
|
||||
getQueueConnection("unmoderated", store),
|
||||
].filter(c => c);
|
||||
connections.forEach(con =>
|
||||
ConnectionHandler.deleteNode(con, input.commentID)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withRejectCommentMutation = createMutationContainer(
|
||||
const RejectCommentMutation = createMutation(
|
||||
"rejectComment",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RejectCommentMutation($input: RejectCommentInput!) {
|
||||
rejectComment(input: $input) {
|
||||
comment {
|
||||
id
|
||||
status
|
||||
}
|
||||
moderationQueues {
|
||||
unmoderated {
|
||||
count
|
||||
}
|
||||
reported {
|
||||
count
|
||||
}
|
||||
pending {
|
||||
count
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
rejectComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: "REJECTED",
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const connections = [
|
||||
getQueueConnection("reported", store),
|
||||
getQueueConnection("pending", store),
|
||||
getQueueConnection("unmoderated", store),
|
||||
].filter(c => c);
|
||||
connections.forEach(con =>
|
||||
ConnectionHandler.deleteNode(con, input.commentID)
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type RejectCommentMutation = (
|
||||
input: RejectCommentInput
|
||||
) => MutationResponsePromise<MutationTypes, "rejectComment">;
|
||||
export default RejectCommentMutation;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { RemoveUserBanMutation as MutationTypes } from "talk-admin/__generated__/RemoveUserBanMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "talk-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RemoveUserBanMutation = createMutation(
|
||||
"removeUserBan",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RemoveUserBanMutation($input: RemoveUserBanInput!) {
|
||||
removeUserBan(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
ban {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeUserBan: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status!.current!.filter(s => s !== GQLUSER_STATUS.BANNED),
|
||||
ban: {
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default RemoveUserBanMutation;
|
||||
@@ -3,7 +3,7 @@ import { Environment, RecordSource } from "relay-runtime";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetAuthViewMutation";
|
||||
import SetAuthViewMutation from "./SetAuthViewMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
@@ -16,6 +16,6 @@ beforeAll(() => {
|
||||
|
||||
it("Sets view", () => {
|
||||
const view = "SIGN_IN";
|
||||
commit(environment, { view }, {} as any);
|
||||
SetAuthViewMutation.commit(environment, { view }, {} as any);
|
||||
expect(source.get(LOCAL_ID)!.authView).toEqual(view);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export interface SetAuthViewInput {
|
||||
@@ -9,20 +8,13 @@ export interface SetAuthViewInput {
|
||||
view: "SIGN_IN" | "ADD_EMAIL_ADDRESS" | "CREATE_USERNAME" | "CREATE_PASSWORD";
|
||||
}
|
||||
|
||||
export type SetAuthViewMutation = (input: SetAuthViewInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetAuthViewInput,
|
||||
{ pym }: TalkContext
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.view, "authView");
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetAuthViewMutation = createMutationContainer(
|
||||
const SetAuthViewMutation = createMutation(
|
||||
"setAuthView",
|
||||
commit
|
||||
(environment: Environment, input: SetAuthViewInput) =>
|
||||
commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.view, "authView");
|
||||
})
|
||||
);
|
||||
|
||||
export default SetAuthViewMutation;
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql";
|
||||
|
||||
export type SetEmailInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetEmailMutation($input: SetEmailInput!) {
|
||||
setEmail(input: $input) {
|
||||
user {
|
||||
email
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetEmailInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
const SetEmailMutation = createMutation(
|
||||
"setEmail",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation SetEmailMutation($input: SetEmailInput!) {
|
||||
setEmail(input: $input) {
|
||||
user {
|
||||
email
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const withSetEmailMutation = createMutationContainer("setEmail", commit);
|
||||
|
||||
export type SetEmailMutation = (
|
||||
input: SetEmailInput
|
||||
) => MutationResponsePromise<MutationTypes, "setEmail">;
|
||||
export default SetEmailMutation;
|
||||
|
||||
@@ -1,49 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql";
|
||||
|
||||
export type SetPasswordInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetPasswordMutation($input: SetPasswordInput!) {
|
||||
setPassword(input: $input) {
|
||||
user {
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetPasswordInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetPasswordMutation = createMutationContainer(
|
||||
const SetPasswordMutation = createMutation(
|
||||
"setPassword",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation SetPasswordMutation($input: SetPasswordInput!) {
|
||||
setPassword(input: $input) {
|
||||
user {
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type SetPasswordMutation = (
|
||||
input: SetPasswordInput
|
||||
) => MutationResponsePromise<MutationTypes, "setPassword">;
|
||||
export default SetPasswordMutation;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createInMemoryStorage,
|
||||
createPromisifiedStorage,
|
||||
} from "talk-framework/lib/storage";
|
||||
import { commit } from "./SetRedirectPathMutation";
|
||||
import SetRedirectPathMutation from "./SetRedirectPathMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
@@ -21,7 +21,7 @@ beforeAll(() => {
|
||||
|
||||
it("Sets redirectPath", async () => {
|
||||
const storage = createInMemoryStorage();
|
||||
await commit(environment, { path: "/path" }, {
|
||||
await SetRedirectPathMutation.commit(environment, { path: "/path" }, {
|
||||
localStorage: createPromisifiedStorage(storage),
|
||||
} as any);
|
||||
expect(source.get(LOCAL_ID)!.redirectPath).toEqual("/path");
|
||||
@@ -30,7 +30,7 @@ it("Sets redirectPath", async () => {
|
||||
|
||||
it("Removes redirectPath", async () => {
|
||||
const storage = createInMemoryStorage();
|
||||
await commit(environment, { path: null }, {
|
||||
await SetRedirectPathMutation.commit(environment, { path: null }, {
|
||||
localStorage: createPromisifiedStorage(storage),
|
||||
} as any);
|
||||
expect(source.get(LOCAL_ID)!.redirectPath).toEqual(null);
|
||||
|
||||
@@ -2,34 +2,30 @@ import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createMutation, LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
export interface SetRedirectPathInput {
|
||||
path: string | null;
|
||||
}
|
||||
|
||||
export type SetRedirectPathMutation = (
|
||||
input: SetRedirectPathInput
|
||||
) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetRedirectPathInput,
|
||||
{ localStorage }: TalkContext
|
||||
) {
|
||||
if (!input.path) {
|
||||
await localStorage.removeItem(REDIRECT_PATH_KEY);
|
||||
} else {
|
||||
await localStorage.setItem(REDIRECT_PATH_KEY, input.path);
|
||||
}
|
||||
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.path, "redirectPath");
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetRedirectPathMutation = createMutationContainer(
|
||||
const SetRedirectPathMutation = createMutation(
|
||||
"setRedirectPath",
|
||||
commit
|
||||
async (
|
||||
environment: Environment,
|
||||
input: SetRedirectPathInput,
|
||||
{ localStorage }: TalkContext
|
||||
) => {
|
||||
if (!input.path) {
|
||||
await localStorage.removeItem(REDIRECT_PATH_KEY);
|
||||
} else {
|
||||
await localStorage.setItem(REDIRECT_PATH_KEY, input.path);
|
||||
}
|
||||
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.path, "redirectPath");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default SetRedirectPathMutation;
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql";
|
||||
|
||||
export type SetUsernameInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetUsernameMutation($input: SetUsernameInput!) {
|
||||
setUsername(input: $input) {
|
||||
user {
|
||||
username
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetUsernameInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetUsernameMutation = createMutationContainer(
|
||||
const SetUsernameMutation = createMutation(
|
||||
"setUsername",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation SetUsernameMutation($input: SetUsernameInput!) {
|
||||
setUsername(input: $input) {
|
||||
user {
|
||||
username
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type SetUsernameMutation = (
|
||||
input: SetUsernameInput
|
||||
) => MutationResponsePromise<MutationTypes, "setUsername">;
|
||||
export default SetUsernameMutation;
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
import { signIn, SignInInput } from "talk-framework/rest";
|
||||
|
||||
export type SignInMutation = (input: SignInInput) => Promise<void>;
|
||||
const SignInMutation = createMutation(
|
||||
"signIn",
|
||||
async (
|
||||
environment: Environment,
|
||||
input: SignInInput,
|
||||
context: TalkContext
|
||||
) => {
|
||||
const result = await signIn(context.rest, input);
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SignInInput,
|
||||
context: TalkContext
|
||||
) {
|
||||
const result = await signIn(context.rest, input);
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await context.clearSession();
|
||||
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
|
||||
// we set the accessToken directly in there.
|
||||
}
|
||||
);
|
||||
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await context.clearSession();
|
||||
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
|
||||
// we set the accessToken directly in there.
|
||||
}
|
||||
|
||||
export const withSignInMutation = createMutationContainer("signIn", commit);
|
||||
export default SignInMutation;
|
||||
|
||||
@@ -1,54 +1,43 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { UpdateSettingsMutation as MutationTypes } from "talk-admin/__generated__/UpdateSettingsMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { UpdateSettingsMutation as MutationTypes } from "talk-admin/__generated__/UpdateSettingsMutation.graphql";
|
||||
|
||||
export type UpdateSettingsInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation UpdateSettingsMutation($input: UpdateSettingsInput!) {
|
||||
updateSettings(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
...AuthConfigContainer_auth
|
||||
}
|
||||
...ModerationConfigContainer_settings
|
||||
...GeneralConfigContainer_settings
|
||||
...OrganizationConfigContainer_settings
|
||||
...WordListConfigContainer_settings
|
||||
...AdvancedConfigContainer_settings
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: UpdateSettingsInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withUpdateSettingsMutation = createMutationContainer(
|
||||
const UpdateSettingsMutation = createMutation(
|
||||
"updateSettings",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateSettingsMutation($input: UpdateSettingsInput!) {
|
||||
updateSettings(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
...AuthConfigContainer_auth
|
||||
}
|
||||
...ModerationConfigContainer_settings
|
||||
...GeneralConfigContainer_settings
|
||||
...OrganizationConfigContainer_settings
|
||||
...WordListConfigContainer_settings
|
||||
...AdvancedConfigContainer_settings
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type UpdateSettingsMutation = (
|
||||
input: UpdateSettingsInput
|
||||
) => MutationResponsePromise<MutationTypes, "updateSettings">;
|
||||
export default UpdateSettingsMutation;
|
||||
|
||||
@@ -1,56 +1,46 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { UpdateUserRoleMutation as MutationTypes } from "talk-admin/__generated__/UpdateUserRoleMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { UpdateUserRoleMutation as MutationTypes } from "talk-admin/__generated__/UpdateUserRoleMutation.graphql";
|
||||
|
||||
export type UpdateUserRoleInput = MutationInput<MutationTypes>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation UpdateUserRoleMutation($input: UpdateUserRoleInput!) {
|
||||
updateUserRole(input: $input) {
|
||||
user {
|
||||
role
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: UpdateUserRoleInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
optimisticResponse: {
|
||||
updateUserRole: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
role: input.role,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withUpdateUserRoleMutation = createMutationContainer(
|
||||
const UpdateUserRoleMutation = createMutation(
|
||||
"updateUserRole",
|
||||
commit
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateUserRoleMutation($input: UpdateUserRoleInput!) {
|
||||
updateUserRole(input: $input) {
|
||||
user {
|
||||
id
|
||||
role
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
optimisticResponse: {
|
||||
updateUserRole: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
role: input.role,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export type UpdateUserRoleMutation = (
|
||||
input: UpdateUserRoleInput
|
||||
) => MutationResponsePromise<MutationTypes, "updateUserRole">;
|
||||
export default UpdateUserRoleMutation;
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
export { withSignInMutation, SignInMutation } from "./SignInMutation";
|
||||
export { default as SignInMutation } from "./SignInMutation";
|
||||
export { default as SetRedirectPathMutation } from "./SetRedirectPathMutation";
|
||||
export { default as AcceptCommentMutation } from "./AcceptCommentMutation";
|
||||
export { default as RejectCommentMutation } from "./RejectCommentMutation";
|
||||
export { default as UpdateSettingsMutation } from "./UpdateSettingsMutation";
|
||||
export {
|
||||
withSetRedirectPathMutation,
|
||||
SetRedirectPathMutation,
|
||||
} from "./SetRedirectPathMutation";
|
||||
export {
|
||||
withAcceptCommentMutation,
|
||||
AcceptCommentMutation,
|
||||
} from "./AcceptCommentMutation";
|
||||
export {
|
||||
withRejectCommentMutation,
|
||||
RejectCommentMutation,
|
||||
} from "./RejectCommentMutation";
|
||||
export {
|
||||
withUpdateSettingsMutation,
|
||||
UpdateSettingsMutation,
|
||||
UpdateSettingsInput,
|
||||
} from "./UpdateSettingsMutation";
|
||||
export {
|
||||
withRegenerateSSOKeyMutation,
|
||||
RegenerateSSOKeyMutation,
|
||||
default as RegenerateSSOKeyMutation,
|
||||
} from "./RegenerateSSOKeyMutation";
|
||||
export {
|
||||
withSetAuthViewMutation,
|
||||
SetAuthViewMutation,
|
||||
} from "./SetAuthViewMutation";
|
||||
export {
|
||||
withClearAuthErrorMutation,
|
||||
ClearAuthErrorMutation,
|
||||
} from "./ClearAuthErrorMutation";
|
||||
export {
|
||||
withCompleteAccountMutation,
|
||||
CompleteAccountMutation,
|
||||
} from "./CompleteAccountMutation";
|
||||
export { withSetEmailMutation, SetEmailMutation } from "./SetEmailMutation";
|
||||
export {
|
||||
withSetUsernameMutation,
|
||||
SetUsernameMutation,
|
||||
} from "./SetUsernameMutation";
|
||||
export {
|
||||
withSetPasswordMutation,
|
||||
SetPasswordMutation,
|
||||
} from "./SetPasswordMutation";
|
||||
export {
|
||||
withUpdateUserRoleMutation,
|
||||
UpdateUserRoleMutation,
|
||||
} from "./UpdateUserRoleMutation";
|
||||
export {
|
||||
OpenStoryInput,
|
||||
withOpenStoryMutation,
|
||||
OpenStoryMutation,
|
||||
} from "./OpenStoryMutation";
|
||||
export {
|
||||
CloseStoryInput,
|
||||
withCloseStoryMutation,
|
||||
CloseStoryMutation,
|
||||
} from "./CloseStoryMutation";
|
||||
export { default as SetAuthViewMutation } from "./SetAuthViewMutation";
|
||||
export { default as ClearAuthErrorMutation } from "./ClearAuthErrorMutation";
|
||||
export { default as CompleteAccountMutation } from "./CompleteAccountMutation";
|
||||
export { default as SetEmailMutation } from "./SetEmailMutation";
|
||||
export { default as SetUsernameMutation } from "./SetUsernameMutation";
|
||||
export { default as SetPasswordMutation } from "./SetPasswordMutation";
|
||||
export { default as UpdateUserRoleMutation } from "./UpdateUserRoleMutation";
|
||||
export { default as OpenStoryMutation } from "./OpenStoryMutation";
|
||||
export { default as CloseStoryMutation } from "./CloseStoryMutation";
|
||||
export { default as BanUserMutation } from "./BanUserMutation";
|
||||
export { default as RemoveUserBanMutation } from "./RemoveUserBanMutation";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.card {
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import NotAvailable from "talk-admin/components/NotAvailable";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import styles from "./BanModal.css";
|
||||
|
||||
interface Props {
|
||||
username: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const BanModal: StatelessComponent<Props> = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
username,
|
||||
}) => (
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="banModal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized
|
||||
id="community-banModal-areYouSure"
|
||||
strong={<strong />}
|
||||
$username={username || <NotAvailable />}
|
||||
>
|
||||
<Typography variant="header2" id="banModal-title">
|
||||
Are you sure you want to ban{" "}
|
||||
<strong>{username || <NotAvailable />}</strong>?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-consequence">
|
||||
<Typography>
|
||||
Once banned, this user will no longer be able to comment, use
|
||||
reactions, or report comments.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<Flex justifyContent="flex-end" itemGutter="half">
|
||||
<Localized id="community-banModal-cancel">
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-banUser">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
onClick={onConfirm}
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Ban User
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export default BanModal;
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
padding-top: 3px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./ButtonPadding.css";
|
||||
|
||||
const ButtonPadding: FunctionComponent = props => (
|
||||
<div className={styles.root}>{props.children}</div>
|
||||
);
|
||||
|
||||
export default ButtonPadding;
|
||||
@@ -5,6 +5,8 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
import { TableCell, TableRow, TextLink } from "talk-ui/components";
|
||||
|
||||
import RoleChangeContainer from "../containers/RoleChangeContainer";
|
||||
import UserStatusChangeContainer from "../containers/UserStatusChangeContainer";
|
||||
import ButtonPadding from "./ButtonPadding";
|
||||
import RoleText from "./RoleText";
|
||||
|
||||
import styles from "./UserRow.css";
|
||||
@@ -16,6 +18,7 @@ interface Props {
|
||||
email: string | null;
|
||||
memberSince: string;
|
||||
role: PropTypesOf<typeof RoleChangeContainer>["role"];
|
||||
user: PropTypesOf<typeof UserStatusChangeContainer>["user"];
|
||||
}
|
||||
|
||||
const UserRow: StatelessComponent<Props> = props => (
|
||||
@@ -35,9 +38,14 @@ const UserRow: StatelessComponent<Props> = props => (
|
||||
{props.canChangeRole ? (
|
||||
<RoleChangeContainer userID={props.userID} role={props.role} />
|
||||
) : (
|
||||
<RoleText>{props.role}</RoleText>
|
||||
<ButtonPadding>
|
||||
<RoleText>{props.role}</RoleText>
|
||||
</ButtonPadding>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={styles.roleColumn}>
|
||||
<UserStatusChangeContainer user={props.user} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.dot {
|
||||
font-size: calc(22rem / var(--rem-base));
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Flex, Typography } from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import styles from "./UserStatus.css";
|
||||
|
||||
interface Props {
|
||||
banned: boolean;
|
||||
suspended: boolean;
|
||||
}
|
||||
|
||||
const render = (
|
||||
color: PropTypesOf<typeof Typography>["color"],
|
||||
content: React.ReactNode
|
||||
) => (
|
||||
<Typography color={color} variant="detail" container="div">
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
<div aria-hidden className={styles.dot}>
|
||||
•
|
||||
</div>
|
||||
{content}
|
||||
</Flex>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const UserStatus: StatelessComponent<Props> = props => {
|
||||
if (props.banned) {
|
||||
return render(
|
||||
"error",
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
<Localized id="userStatus-banned">
|
||||
<div>Banned</div>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
if (props.suspended) {
|
||||
return render(
|
||||
"warning",
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
<Localized id="userStatus-suspended">
|
||||
<div>Suspended</div>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return render(
|
||||
"success",
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
<Localized id="userStatus-active">
|
||||
<div>Active</div>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserStatus;
|
||||
@@ -0,0 +1,9 @@
|
||||
.button {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdownButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
ClickOutside,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Popover,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import styles from "./UserStatusChange.css";
|
||||
|
||||
interface Props {
|
||||
onBan: () => void;
|
||||
onRemoveBan: () => void;
|
||||
onSuspend: () => void;
|
||||
onRemoveSuspension: () => void;
|
||||
banned: boolean;
|
||||
suspended: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const UserStatusChange: StatelessComponent<Props> = props => (
|
||||
<Localized id="community-userStatus-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
id="community-statusChange"
|
||||
placement="bottom-start"
|
||||
description="A dropdown to change the user status"
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside onClickOutside={toggleVisibility}>
|
||||
<Dropdown>
|
||||
{!props.banned && (
|
||||
<Localized id="community-userStatus-banUser">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
props.onBan();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Ban User
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{props.banned && (
|
||||
<Localized id="community-userStatus-removeBan">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
props.onRemoveBan();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Remove Ban
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{!props.suspended && (
|
||||
<Localized id="community-userStatus-suspendUser">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
props.onSuspend();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Suspend User
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{props.suspended && (
|
||||
<Localized id="community-userStatus-removeSuspension">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
props.onRemoveSuspension();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Remove Suspension
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
</Dropdown>
|
||||
</ClickOutside>
|
||||
)}
|
||||
>
|
||||
{({ toggleVisibility, ref, visible }) => (
|
||||
<Localized
|
||||
id="community-userStatus-changeButton"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<Button
|
||||
aria-label="Change user status"
|
||||
className={styles.button}
|
||||
onClick={toggleVisibility}
|
||||
ref={ref}
|
||||
variant="regular"
|
||||
size="small"
|
||||
>
|
||||
{props.children}
|
||||
{
|
||||
<ButtonIcon size="lg">
|
||||
{visible ? "arrow_drop_up" : "arrow_drop_down"}
|
||||
</ButtonIcon>
|
||||
}
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Popover>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default UserStatusChange;
|
||||
@@ -2,11 +2,14 @@
|
||||
width: 25%;
|
||||
}
|
||||
.emailColumn {
|
||||
width: 40%;
|
||||
width: 30%;
|
||||
}
|
||||
.memberSinceColumn {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
}
|
||||
.roleColumn {
|
||||
width: 15%;
|
||||
}
|
||||
.statusColumn {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ const UserTable: StatelessComponent<Props> = props => (
|
||||
<Localized id="community-column-role">
|
||||
<TableCell className={styles.roleColumn}>Role</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-status">
|
||||
<TableCell className={styles.statusColumn}>Status</TableCell>
|
||||
</Localized>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
.adornment {
|
||||
padding: 0 var(--spacing-unit);
|
||||
}
|
||||
|
||||
.selectField {
|
||||
min-width: calc(13 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { GQLUSER_ROLE, GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
import {
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_ROLE_RL,
|
||||
GQLUSER_STATUS,
|
||||
GQLUSER_STATUS_RL,
|
||||
} from "talk-framework/schema";
|
||||
import {
|
||||
Button,
|
||||
FieldSet,
|
||||
@@ -20,6 +25,8 @@ import styles from "./UserTableFilter.css";
|
||||
interface Props {
|
||||
roleFilter: GQLUSER_ROLE_RL | null;
|
||||
onSetRoleFilter: (role: GQLUSER_ROLE_RL) => void;
|
||||
statusFilter: GQLUSER_STATUS_RL | null;
|
||||
onSetStatusFilter: (role: GQLUSER_STATUS_RL) => void;
|
||||
searchFilter: string;
|
||||
onSetSearchFilter: (search: string) => void;
|
||||
}
|
||||
@@ -91,46 +98,77 @@ const UserTableFilter: StatelessComponent<Props> = props => (
|
||||
Show Me
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-roleSelectField"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<SelectField
|
||||
aria-label="Search by role"
|
||||
value={props.roleFilter || ""}
|
||||
onChange={e => props.onSetRoleFilter((e.target.value as any) || null)}
|
||||
<Flex itemGutter>
|
||||
<Localized
|
||||
id="community-filter-roleSelectField"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<Localized id="community-filter-everyone">
|
||||
<Option value="">Everyone</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-optGroupAudience"
|
||||
attrs={{ label: true }}
|
||||
<SelectField
|
||||
aria-label="Search by role"
|
||||
value={props.roleFilter || ""}
|
||||
className={styles.selectField}
|
||||
onChange={e =>
|
||||
props.onSetRoleFilter((e.target.value as any) || null)
|
||||
}
|
||||
>
|
||||
<OptGroup label="Audience">
|
||||
<Localized id="role-plural-commenter">
|
||||
<Option value={GQLUSER_ROLE.COMMENTER}>Commenters</Option>
|
||||
</Localized>
|
||||
</OptGroup>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-optGroupOrganization"
|
||||
attrs={{ label: true }}
|
||||
<Localized id="community-filter-allRoles">
|
||||
<Option value="">All Roles</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-optGroupAudience"
|
||||
attrs={{ label: true }}
|
||||
>
|
||||
<OptGroup label="Audience">
|
||||
<Localized id="role-plural-commenter">
|
||||
<Option value={GQLUSER_ROLE.COMMENTER}>Commenters</Option>
|
||||
</Localized>
|
||||
</OptGroup>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-optGroupOrganization"
|
||||
attrs={{ label: true }}
|
||||
>
|
||||
<OptGroup label="Organization">
|
||||
<Localized id="role-plural-admin">
|
||||
<Option value={GQLUSER_ROLE.ADMIN}>Admins</Option>
|
||||
</Localized>
|
||||
<Localized id="role-plural-moderator">
|
||||
<Option value={GQLUSER_ROLE.MODERATOR}>Moderators</Option>
|
||||
</Localized>
|
||||
<Localized id="role-plural-staff">
|
||||
<Option value={GQLUSER_ROLE.STAFF}>Staff</Option>
|
||||
</Localized>
|
||||
</OptGroup>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="community-filter-statusSelectField"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<SelectField
|
||||
aria-label="Search by status"
|
||||
value={props.statusFilter || ""}
|
||||
className={styles.selectField}
|
||||
onChange={e =>
|
||||
props.onSetStatusFilter((e.target.value as any) || null)
|
||||
}
|
||||
>
|
||||
<OptGroup label="Organization">
|
||||
<Localized id="role-plural-admin">
|
||||
<Option value={GQLUSER_ROLE.ADMIN}>Admins</Option>
|
||||
</Localized>
|
||||
<Localized id="role-plural-moderator">
|
||||
<Option value={GQLUSER_ROLE.MODERATOR}>Moderators</Option>
|
||||
</Localized>
|
||||
<Localized id="role-plural-staff">
|
||||
<Option value={GQLUSER_ROLE.STAFF}>Staff</Option>
|
||||
</Localized>
|
||||
</OptGroup>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</Localized>
|
||||
<Localized id="community-filter-allStatuses">
|
||||
<Option value="">All Statuses</Option>
|
||||
</Localized>
|
||||
<Localized id="userStatus-active">
|
||||
<Option value={GQLUSER_STATUS.ACTIVE}>Active</Option>
|
||||
</Localized>
|
||||
<Localized id="userStatus-suspended">
|
||||
<Option value={GQLUSER_STATUS.SUSPENDED}>Suspended</Option>
|
||||
</Localized>
|
||||
<Localized id="userStatus-banned">
|
||||
<Option value={GQLUSER_STATUS.BANNED}>Banned</Option>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</FieldSet>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { StatelessComponent, useCallback } from "react";
|
||||
|
||||
import {
|
||||
UpdateUserRoleMutation,
|
||||
withUpdateUserRoleMutation,
|
||||
} from "talk-admin/mutations/UpdateUserRoleMutation";
|
||||
import { UpdateUserRoleMutation } from "talk-admin/mutations";
|
||||
import { useMutation } from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
|
||||
import RoleChange from "../components/RoleChange";
|
||||
@@ -11,22 +9,20 @@ import RoleChange from "../components/RoleChange";
|
||||
interface Props {
|
||||
userID: string;
|
||||
role: GQLUSER_ROLE_RL;
|
||||
updateUserRole: UpdateUserRoleMutation;
|
||||
}
|
||||
|
||||
const RoleChangeContainer: StatelessComponent<Props> = props => {
|
||||
const updateUserRole = useMutation(UpdateUserRoleMutation);
|
||||
const handleOnChangeRole = useCallback(
|
||||
(role: GQLUSER_ROLE_RL) => {
|
||||
if (role === props.role) {
|
||||
return;
|
||||
}
|
||||
props.updateUserRole({ userID: props.userID, role });
|
||||
updateUserRole({ userID: props.userID, role });
|
||||
},
|
||||
[props.userID, props.role, props.updateUserRole]
|
||||
[props.userID, props.role, updateUserRole]
|
||||
);
|
||||
return <RoleChange onChangeRole={handleOnChangeRole} role={props.role} />;
|
||||
};
|
||||
|
||||
const enhanced = withUpdateUserRoleMutation(RoleChangeContainer);
|
||||
|
||||
export default enhanced;
|
||||
export default RoleChangeContainer;
|
||||
|
||||
@@ -18,6 +18,7 @@ const UserRowContainer: StatelessComponent<Props> = props => {
|
||||
const { locales } = useTalkContext();
|
||||
return (
|
||||
<UserRow
|
||||
user={props.user}
|
||||
userID={props.user.id}
|
||||
username={props.user.username!}
|
||||
email={props.user.email}
|
||||
@@ -44,6 +45,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
`,
|
||||
user: graphql`
|
||||
fragment UserRowContainer_user on User {
|
||||
...UserStatusChangeContainer_user
|
||||
id
|
||||
username
|
||||
email
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { StatelessComponent, useCallback, useState } from "react";
|
||||
|
||||
import { UserStatusChangeContainer_user as UserData } from "talk-admin/__generated__/UserStatusChangeContainer_user.graphql";
|
||||
import { BanUserMutation, RemoveUserBanMutation } from "talk-admin/mutations";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
import BanModal from "../components/BanModal";
|
||||
import ButtonPadding from "../components/ButtonPadding";
|
||||
import UserStatusChange from "../components/UserStatusChange";
|
||||
import UserStatusContainer from "./UserStatusContainer";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
const UserStatusChangeContainer: StatelessComponent<Props> = props => {
|
||||
const banUser = useMutation(BanUserMutation);
|
||||
const removeUserBan = useMutation(RemoveUserBanMutation);
|
||||
const [showBanned, setShowBanned] = useState<boolean>(false);
|
||||
const handleBan = useCallback(
|
||||
() => {
|
||||
if (props.user.status.ban.active) {
|
||||
return;
|
||||
}
|
||||
setShowBanned(true);
|
||||
},
|
||||
[props.user, setShowBanned]
|
||||
);
|
||||
const handleRemoveBan = useCallback(
|
||||
() => {
|
||||
if (!props.user.status.ban.active) {
|
||||
return;
|
||||
}
|
||||
removeUserBan({ userID: props.user.id });
|
||||
},
|
||||
[props.user, removeUserBan]
|
||||
);
|
||||
const handleSuspend = useCallback(
|
||||
() => {
|
||||
if (props.user.status.suspension.active) {
|
||||
return;
|
||||
}
|
||||
// TODO: (cvle)
|
||||
},
|
||||
[props.user]
|
||||
);
|
||||
const handleRemoveSuspension = useCallback(
|
||||
() => {
|
||||
if (!props.user.status.suspension.active) {
|
||||
return;
|
||||
}
|
||||
// TODO: (cvle)
|
||||
},
|
||||
[props.user]
|
||||
);
|
||||
|
||||
if (props.user.role !== GQLUSER_ROLE.COMMENTER) {
|
||||
return (
|
||||
<ButtonPadding>
|
||||
<UserStatusContainer user={props.user} />
|
||||
</ButtonPadding>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserStatusChange
|
||||
onBan={handleBan}
|
||||
onRemoveBan={handleRemoveBan}
|
||||
onSuspend={handleSuspend}
|
||||
onRemoveSuspension={handleRemoveSuspension}
|
||||
banned={props.user.status.ban.active}
|
||||
suspended={props.user.status.suspension.active}
|
||||
>
|
||||
<UserStatusContainer user={props.user} />
|
||||
</UserStatusChange>
|
||||
<BanModal
|
||||
username={props.user.username}
|
||||
open={showBanned}
|
||||
onClose={() => setShowBanned(false)}
|
||||
onConfirm={() => {
|
||||
banUser({ userID: props.user.id });
|
||||
setShowBanned(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserStatusChangeContainer_user on User {
|
||||
...UserStatusContainer_user
|
||||
id
|
||||
role
|
||||
username
|
||||
status {
|
||||
ban {
|
||||
active
|
||||
}
|
||||
suspension {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(UserStatusChangeContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserStatusContainer_user as UserData } from "talk-admin/__generated__/UserStatusContainer_user.graphql";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { GQLUSER_STATUS } from "talk-framework/schema";
|
||||
|
||||
import UserStatus from "../components/UserStatus";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
const UserStatusContainer: StatelessComponent<Props> = props => {
|
||||
return (
|
||||
<UserStatus
|
||||
banned={props.user.status.current.includes(GQLUSER_STATUS.BANNED)}
|
||||
suspended={props.user.status.current.includes(GQLUSER_STATUS.SUSPENDED)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserStatusContainer_user on User {
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(UserStatusContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useRefetch,
|
||||
withPaginationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
import { GQLUSER_ROLE_RL, GQLUSER_STATUS_RL } from "talk-framework/schema";
|
||||
|
||||
import { HorizontalGutter } from "talk-ui/components";
|
||||
import UserTable from "../components/UserTable";
|
||||
@@ -28,14 +28,13 @@ const UserTableContainer: StatelessComponent<Props> = props => {
|
||||
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||
const [searchFilter, setSearchFilter] = useState<string>("");
|
||||
const [roleFilter, setRoleFilter] = useState<GQLUSER_ROLE_RL | null>(null);
|
||||
const [, isRefetching] = useRefetch<
|
||||
Pick<
|
||||
UserTableContainerPaginationQueryVariables,
|
||||
"searchFilter" | "roleFilter"
|
||||
>
|
||||
>(props.relay, {
|
||||
const [statusFilter, setStatusFilter] = useState<GQLUSER_STATUS_RL | null>(
|
||||
null
|
||||
);
|
||||
const [, isRefetching] = useRefetch(props.relay, {
|
||||
searchFilter: searchFilter || null,
|
||||
roleFilter,
|
||||
statusFilter,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -43,7 +42,9 @@ const UserTableContainer: StatelessComponent<Props> = props => {
|
||||
<HorizontalGutter size="double">
|
||||
<UserTableFilter
|
||||
onSetRoleFilter={setRoleFilter}
|
||||
onSetStatusFilter={setStatusFilter}
|
||||
roleFilter={roleFilter}
|
||||
statusFilter={statusFilter}
|
||||
onSetSearchFilter={setSearchFilter}
|
||||
searchFilter={searchFilter}
|
||||
/>
|
||||
@@ -75,6 +76,7 @@ const enhanced = withPaginationContainer<
|
||||
count: { type: "Int!", defaultValue: 10 }
|
||||
cursor: { type: "Cursor" }
|
||||
roleFilter: { type: "USER_ROLE" }
|
||||
statusFilter: { type: "USER_STATUS" }
|
||||
searchFilter: { type: "String" }
|
||||
) {
|
||||
viewer {
|
||||
@@ -84,6 +86,7 @@ const enhanced = withPaginationContainer<
|
||||
first: $count
|
||||
after: $cursor
|
||||
role: $roleFilter
|
||||
status: $statusFilter
|
||||
query: $searchFilter
|
||||
) @connection(key: "UserTable_users") {
|
||||
edges {
|
||||
@@ -113,6 +116,7 @@ const enhanced = withPaginationContainer<
|
||||
count,
|
||||
cursor,
|
||||
roleFilter: fragmentVariables.roleFilter,
|
||||
statusFilter: fragmentVariables.statusFilter,
|
||||
searchFilter: fragmentVariables.searchFilter,
|
||||
};
|
||||
},
|
||||
@@ -123,6 +127,7 @@ const enhanced = withPaginationContainer<
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$roleFilter: USER_ROLE
|
||||
$statusFilter: USER_STATUS
|
||||
$searchFilter: String
|
||||
) {
|
||||
...UserTableContainer_query
|
||||
@@ -130,6 +135,7 @@ const enhanced = withPaginationContainer<
|
||||
count: $count
|
||||
cursor: $cursor
|
||||
roleFilter: $roleFilter
|
||||
statusFilter: $statusFilter
|
||||
searchFilter: $searchFilter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
UpdateSettingsInput,
|
||||
UpdateSettingsMutation,
|
||||
withUpdateSettingsMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { UpdateSettingsMutation } from "talk-admin/mutations";
|
||||
import { SubmitHookHandler } from "talk-framework/lib/form";
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
|
||||
import Configure from "../components/Configure";
|
||||
import NavigationWarningContainer from "./NavigationWarningContainer";
|
||||
|
||||
interface Props {
|
||||
updateSettings: UpdateSettingsMutation;
|
||||
updateSettings: MutationProp<typeof UpdateSettingsMutation>;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
@@ -26,7 +23,7 @@ class ConfigureContainer extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
private handleExecute = async (
|
||||
data: UpdateSettingsInput["settings"],
|
||||
data: Parameters<Props["updateSettings"]>[0]["settings"],
|
||||
form: FormApi
|
||||
) => {
|
||||
await this.props.updateSettings({ settings: data });
|
||||
@@ -55,5 +52,5 @@ class ConfigureContainer extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withUpdateSettingsMutation(ConfigureContainer);
|
||||
const enhanced = withMutation(UpdateSettingsMutation)(ConfigureContainer);
|
||||
export default enhanced;
|
||||
|
||||
+7
-6
@@ -2,18 +2,19 @@ import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { SSOKeyFieldContainer_sso as SSOData } from "talk-admin/__generated__/SSOKeyFieldContainer_sso.graphql";
|
||||
import { RegenerateSSOKeyMutation } from "talk-admin/mutations";
|
||||
import {
|
||||
RegenerateSSOKeyMutation,
|
||||
withRegenerateSSOKeyMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import SSOKeyField from "../components/SSOKeyField";
|
||||
|
||||
interface Props {
|
||||
sso: SSOData;
|
||||
disabled?: boolean;
|
||||
regenerateSSOKey: RegenerateSSOKeyMutation;
|
||||
regenerateSSOKey: MutationProp<typeof RegenerateSSOKeyMutation>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -44,7 +45,7 @@ class SSOKeyFieldContainer extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRegenerateSSOKeyMutation(
|
||||
const enhanced = withMutation(RegenerateSSOKeyMutation)(
|
||||
withFragmentContainer<Props>({
|
||||
sso: graphql`
|
||||
fragment SSOKeyFieldContainer_sso on SSOAuthIntegration {
|
||||
|
||||
@@ -9,23 +9,22 @@ import {
|
||||
CompleteAccountMutation,
|
||||
SetAuthViewMutation,
|
||||
SetRedirectPathMutation,
|
||||
withCompleteAccountMutation,
|
||||
withSetAuthViewMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import {
|
||||
graphql,
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
withMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
type Props = {
|
||||
completeAccount: CompleteAccountMutation;
|
||||
setAuthView: SetAuthViewMutation;
|
||||
completeAccount: MutationProp<typeof CompleteAccountMutation>;
|
||||
setAuthView: MutationProp<typeof SetAuthViewMutation>;
|
||||
local: Local;
|
||||
auth: AuthData;
|
||||
viewer: UserData | null;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
setRedirectPath: MutationProp<typeof SetRedirectPathMutation>;
|
||||
} & WithRouter;
|
||||
|
||||
function handleAccountCompletion(props: Props) {
|
||||
@@ -132,9 +131,11 @@ const enhanced = withLocalStateContainer(
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withSetAuthViewMutation(
|
||||
withSetRedirectPathMutation(
|
||||
withCompleteAccountMutation(withRouter(AccountCompletionContainer))
|
||||
withMutation(SetAuthViewMutation)(
|
||||
withMutation(SetRedirectPathMutation)(
|
||||
withMutation(CompleteAccountMutation)(
|
||||
withRouter(AccountCompletionContainer)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
+4
-6
@@ -1,16 +1,14 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetEmailMutation,
|
||||
withSetEmailMutation,
|
||||
} from "talk-admin/mutations/SetEmailMutation";
|
||||
import { SetEmailMutation } from "talk-admin/mutations";
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import AddEmailAddress from "../components/AddEmailAddress";
|
||||
|
||||
interface Props {
|
||||
setEmail: SetEmailMutation;
|
||||
setEmail: MutationProp<typeof SetEmailMutation>;
|
||||
}
|
||||
|
||||
class AddEmailAddressContainer extends Component<Props> {
|
||||
@@ -31,5 +29,5 @@ class AddEmailAddressContainer extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetEmailMutation(AddEmailAddressContainer);
|
||||
const enhanced = withMutation(SetEmailMutation)(AddEmailAddressContainer);
|
||||
export default enhanced;
|
||||
|
||||
+4
-6
@@ -1,16 +1,14 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetPasswordMutation,
|
||||
withSetPasswordMutation,
|
||||
} from "talk-admin/mutations/SetPasswordMutation";
|
||||
import { SetPasswordMutation } from "talk-admin/mutations";
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import CreatePassword from "../components/CreatePassword";
|
||||
|
||||
interface Props {
|
||||
setPassword: SetPasswordMutation;
|
||||
setPassword: MutationProp<typeof SetPasswordMutation>;
|
||||
}
|
||||
|
||||
class CreatePasswordContainer extends Component<Props> {
|
||||
@@ -32,5 +30,5 @@ class CreatePasswordContainer extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetPasswordMutation(CreatePasswordContainer);
|
||||
const enhanced = withMutation(SetPasswordMutation)(CreatePasswordContainer);
|
||||
export default enhanced;
|
||||
|
||||
+4
-6
@@ -1,16 +1,14 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetUsernameMutation,
|
||||
withSetUsernameMutation,
|
||||
} from "talk-admin/mutations/SetUsernameMutation";
|
||||
import { SetUsernameMutation } from "talk-admin/mutations";
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import CreateUsername from "../components/CreateUsername";
|
||||
|
||||
interface Props {
|
||||
setUsername: SetUsernameMutation;
|
||||
setUsername: MutationProp<typeof SetUsernameMutation>;
|
||||
}
|
||||
|
||||
class CreateUsernameContainer extends Component<Props> {
|
||||
@@ -32,5 +30,5 @@ class CreateUsernameContainer extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetUsernameMutation(CreateUsernameContainer);
|
||||
const enhanced = withMutation(SetUsernameMutation)(CreateUsernameContainer);
|
||||
export default enhanced;
|
||||
|
||||
@@ -3,16 +3,13 @@ import React, { Component } from "react";
|
||||
|
||||
import { SignInContainer_auth as AuthData } from "talk-admin/__generated__/SignInContainer_auth.graphql";
|
||||
import { SignInContainerLocal as LocalData } from "talk-admin/__generated__/SignInContainerLocal.graphql";
|
||||
import {
|
||||
ClearAuthErrorMutation,
|
||||
SignInMutation,
|
||||
withClearAuthErrorMutation,
|
||||
withSignInMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { ClearAuthErrorMutation, SignInMutation } from "talk-admin/mutations";
|
||||
import {
|
||||
graphql,
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
withMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import SignIn from "../components/SignIn";
|
||||
@@ -21,8 +18,8 @@ interface Props {
|
||||
local: LocalData;
|
||||
auth: AuthData;
|
||||
error?: Error | null;
|
||||
signIn: SignInMutation;
|
||||
clearAuthError: ClearAuthErrorMutation;
|
||||
signIn: MutationProp<typeof SignInMutation>;
|
||||
clearAuthError: MutationProp<typeof ClearAuthErrorMutation>;
|
||||
}
|
||||
|
||||
class SignInContainer extends Component<Props> {
|
||||
@@ -91,8 +88,8 @@ const enhanced = withFragmentContainer<Props>({
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withClearAuthErrorMutation(
|
||||
withSignInMutation(
|
||||
withMutation(ClearAuthErrorMutation)(
|
||||
withMutation(SignInMutation)(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment SignInContainerLocal on Local {
|
||||
|
||||
+4
-3
@@ -1,14 +1,15 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { SignInMutation, withSignInMutation } from "talk-admin/mutations";
|
||||
import { SignInMutation } from "talk-admin/mutations";
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
|
||||
import SignInWithEmail, {
|
||||
SignInWithEmailForm,
|
||||
} from "../components/SignInWithEmail";
|
||||
|
||||
interface SignInContainerProps {
|
||||
signIn: SignInMutation;
|
||||
signIn: MutationProp<typeof SignInMutation>;
|
||||
}
|
||||
|
||||
class SignInContainer extends Component<SignInContainerProps> {
|
||||
@@ -25,5 +26,5 @@ class SignInContainer extends Component<SignInContainerProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSignInMutation(SignInContainer);
|
||||
const enhanced = withMutation(SignInMutation)(SignInContainer);
|
||||
export default enhanced;
|
||||
|
||||
@@ -6,23 +6,21 @@ import {
|
||||
ModerateCardContainer_comment as CommentData,
|
||||
} from "talk-admin/__generated__/ModerateCardContainer_comment.graphql";
|
||||
import { ModerateCardContainer_settings as SettingsData } from "talk-admin/__generated__/ModerateCardContainer_settings.graphql";
|
||||
import { AcceptCommentMutation } from "talk-admin/mutations";
|
||||
import { RejectCommentMutation } from "talk-admin/mutations";
|
||||
import {
|
||||
AcceptCommentMutation,
|
||||
withAcceptCommentMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import {
|
||||
RejectCommentMutation,
|
||||
withRejectCommentMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import ModerateCard from "../components/ModerateCard";
|
||||
|
||||
interface ModerateCardContainerProps {
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
acceptComment: AcceptCommentMutation;
|
||||
rejectComment: RejectCommentMutation;
|
||||
acceptComment: MutationProp<typeof AcceptCommentMutation>;
|
||||
rejectComment: MutationProp<typeof RejectCommentMutation>;
|
||||
danglingLogic: (status: COMMENT_STATUS) => boolean;
|
||||
}
|
||||
|
||||
@@ -106,6 +104,10 @@ const enhanced = withFragmentContainer<ModerateCardContainerProps>({
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(withAcceptCommentMutation(withRejectCommentMutation(ModerateCardContainer)));
|
||||
})(
|
||||
withMutation(AcceptCommentMutation)(
|
||||
withMutation(RejectCommentMutation)(ModerateCardContainer)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import React, { StatelessComponent, useCallback } from "react";
|
||||
|
||||
import {
|
||||
CloseStoryMutation,
|
||||
OpenStoryMutation,
|
||||
withCloseStoryMutation,
|
||||
withOpenStoryMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { CloseStoryMutation, OpenStoryMutation } from "talk-admin/mutations";
|
||||
import { GQLSTORY_STATUS, GQLSTORY_STATUS_RL } from "talk-framework/schema";
|
||||
|
||||
import { MutationProp, withMutation } from "talk-framework/lib/relay";
|
||||
import StatusChange from "../components/StatusChange";
|
||||
|
||||
interface Props {
|
||||
storyID: string;
|
||||
status: GQLSTORY_STATUS_RL;
|
||||
openStory: OpenStoryMutation;
|
||||
closeStory: CloseStoryMutation;
|
||||
openStory: MutationProp<typeof OpenStoryMutation>;
|
||||
closeStory: MutationProp<typeof CloseStoryMutation>;
|
||||
}
|
||||
|
||||
const StatusChangeContainer: StatelessComponent<Props> = props => {
|
||||
@@ -36,8 +32,8 @@ const StatusChangeContainer: StatelessComponent<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withOpenStoryMutation(
|
||||
withCloseStoryMutation(StatusChangeContainer)
|
||||
const enhanced = withMutation(OpenStoryMutation)(
|
||||
withMutation(CloseStoryMutation)(StatusChangeContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -29,7 +29,7 @@ async function createTestRenderer(
|
||||
.stub()
|
||||
.returns(
|
||||
merge(
|
||||
{ ...users[0], email: "", username: "", profiles: [] },
|
||||
{ ...users.admins[0], email: "", username: "", profiles: [] },
|
||||
get(customResolver, "Query.viewer")
|
||||
)
|
||||
),
|
||||
|
||||
@@ -25,7 +25,7 @@ async function createTestRenderer(
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns({ ...users[0], email: "" }),
|
||||
viewer: sinon.stub().returns({ ...users.admins[0], email: "" }),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ async function createTestRenderer(
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns({ ...users[0], profiles: [] }),
|
||||
viewer: sinon.stub().returns({ ...users.admins[0], profiles: [] }),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const resolvers = {
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ function createTestRenderer(
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
viewer: sinon.stub().returns({ ...users[0], ...userDiff }),
|
||||
viewer: sinon.stub().returns({ ...users.admins[0], ...userDiff }),
|
||||
},
|
||||
};
|
||||
const { testRenderer, context } = create({
|
||||
|
||||
@@ -21,7 +21,7 @@ const resolvers = {
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ it("logs out", async () => {
|
||||
.returns({});
|
||||
|
||||
const userMenu = await waitForElement(() =>
|
||||
within(testRenderer.root).getByText(users[0].username, {
|
||||
within(testRenderer.root).getByText(users.admins[0].username!, {
|
||||
selector: "button",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ban user 1`] = `
|
||||
<div
|
||||
aria-labelledby="banModal-title"
|
||||
className="Modal-root"
|
||||
onKeyDown={[Function]}
|
||||
role="modal"
|
||||
>
|
||||
<div
|
||||
className="Backdrop-root Backdrop-active"
|
||||
data-testid="backdrop"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<div
|
||||
onFocus={[Function]}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
className="Card-root BanModal-card"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root CloseButton-root"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md CloseButton-icon"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-header2 Typography-colorTextPrimary"
|
||||
id="banModal-title"
|
||||
>
|
||||
Are you sure you want to ban
|
||||
<strong>
|
||||
Isabelle
|
||||
</strong>
|
||||
?
|
||||
</h1>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Once banned, this user will no longer be able to comment, use
|
||||
reactions, or report comments.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Ban User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onFocus={[Function]}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders community 1`] = `
|
||||
<div
|
||||
className="MainLayout-root Community-root"
|
||||
@@ -68,63 +167,111 @@ exports[`renders community 1`] = `
|
||||
>
|
||||
Show Me
|
||||
</legend>
|
||||
<span
|
||||
className="SelectField-root"
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-itemGutter"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
<span
|
||||
className="SelectField-root UserTableFilter-selectField"
|
||||
>
|
||||
<option
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
Everyone
|
||||
</option>
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
<option
|
||||
value="COMMENTER"
|
||||
value=""
|
||||
>
|
||||
Commenters
|
||||
All Roles
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
<option
|
||||
value="COMMENTER"
|
||||
>
|
||||
Commenters
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
expand_more
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="SelectField-root UserTableFilter-selectField"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by user status"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All Statuses
|
||||
</option>
|
||||
<option
|
||||
value="ACTIVE"
|
||||
>
|
||||
Active
|
||||
</option>
|
||||
<option
|
||||
value="SUSPENDED"
|
||||
>
|
||||
Suspended
|
||||
</option>
|
||||
<option
|
||||
value="BANNED"
|
||||
>
|
||||
Banned
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
@@ -159,6 +306,11 @@ exports[`renders community 1`] = `
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-statusColumn TableCell-header"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
@@ -190,11 +342,40 @@ exports[`renders community 1`] = `
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<span
|
||||
className="RoleText-root"
|
||||
<div
|
||||
className="ButtonPadding-root"
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
<span
|
||||
className="RoleText-root"
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="ButtonPadding-root"
|
||||
>
|
||||
<div
|
||||
className="Typography-root Typography-detail Typography-colorSuccess"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
className="UserStatus-dot"
|
||||
>
|
||||
•
|
||||
</div>
|
||||
<div>
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@@ -264,6 +445,248 @@ exports[`renders community 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="ButtonPadding-root"
|
||||
>
|
||||
<div
|
||||
className="Typography-root Typography-detail Typography-colorSuccess"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
className="UserStatus-dot"
|
||||
>
|
||||
•
|
||||
</div>
|
||||
<div>
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Huy
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
>
|
||||
<a
|
||||
className="TextLink-root"
|
||||
href="mailto:huy@test.com"
|
||||
>
|
||||
huy@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-memberSinceColumn TableCell-body"
|
||||
>
|
||||
07/06/2018
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
aria-label="Change role"
|
||||
className="BaseButton-root Button-root RoleChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="RoleText-root"
|
||||
>
|
||||
Staff
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-lg ButtonIcon-root"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="community-roleChange-ariainfo"
|
||||
id="community-roleChange"
|
||||
role="popup"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="community-roleChange-ariainfo"
|
||||
>
|
||||
A dropdown to change the user role
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="ButtonPadding-root"
|
||||
>
|
||||
<div
|
||||
className="Typography-root Typography-detail Typography-colorSuccess"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
className="UserStatus-dot"
|
||||
>
|
||||
•
|
||||
</div>
|
||||
<div>
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Isabelle
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
>
|
||||
<a
|
||||
className="TextLink-root"
|
||||
href="mailto:isabelle@test.com"
|
||||
>
|
||||
isabelle@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-memberSinceColumn TableCell-body"
|
||||
>
|
||||
07/06/2018
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
aria-label="Change role"
|
||||
className="BaseButton-root Button-root RoleChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="RoleText-root RoleText-commenter"
|
||||
>
|
||||
Commenter
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-lg ButtonIcon-root"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="community-roleChange-ariainfo"
|
||||
id="community-roleChange"
|
||||
role="popup"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="community-roleChange-ariainfo"
|
||||
>
|
||||
A dropdown to change the user role
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-roleColumn TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
aria-label="Change user status"
|
||||
className="BaseButton-root Button-root UserStatusChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
className="Typography-root Typography-detail Typography-colorSuccess"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
className="UserStatus-dot"
|
||||
>
|
||||
•
|
||||
</div>
|
||||
<div>
|
||||
Active
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-lg ButtonIcon-root"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="community-statusChange-ariainfo"
|
||||
id="community-statusChange"
|
||||
role="popup"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="community-statusChange-ariainfo"
|
||||
>
|
||||
A dropdown to change the user status
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -340,63 +763,111 @@ exports[`renders empty community 1`] = `
|
||||
>
|
||||
Show Me
|
||||
</legend>
|
||||
<span
|
||||
className="SelectField-root"
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-itemGutter"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
<span
|
||||
className="SelectField-root UserTableFilter-selectField"
|
||||
>
|
||||
<option
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
Everyone
|
||||
</option>
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
<option
|
||||
value="COMMENTER"
|
||||
value=""
|
||||
>
|
||||
Commenters
|
||||
All Roles
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
<option
|
||||
value="COMMENTER"
|
||||
>
|
||||
Commenters
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
expand_more
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="SelectField-root UserTableFilter-selectField"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by user status"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All Statuses
|
||||
</option>
|
||||
<option
|
||||
value="ACTIVE"
|
||||
>
|
||||
Active
|
||||
</option>
|
||||
<option
|
||||
value="SUSPENDED"
|
||||
>
|
||||
Suspended
|
||||
</option>
|
||||
<option
|
||||
value="BANNED"
|
||||
>
|
||||
Banned
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
@@ -431,6 +902,11 @@ exports[`renders empty community 1`] = `
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-statusColumn TableCell-header"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { get, merge } from "lodash";
|
||||
import { merge } from "lodash";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import {
|
||||
createSinonStub,
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_STATUS,
|
||||
QueryToSettingsResolver,
|
||||
QueryToUsersResolver,
|
||||
QueryToViewerResolver,
|
||||
} from "talk-framework/schema";
|
||||
import {
|
||||
createMutationResolverStub,
|
||||
createQueryResolverStub,
|
||||
findParentWithType,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
@@ -12,6 +18,11 @@ import {
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import {
|
||||
BanUserMutation,
|
||||
RemoveUserBanMutation,
|
||||
UpdateUserRoleMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import create from "../create";
|
||||
import {
|
||||
communityUsers,
|
||||
@@ -28,14 +39,16 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
const resolvers = {
|
||||
...resolver,
|
||||
Query: {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
users: sinon.stub().callsFake((_, data) => {
|
||||
expectAndFail(data.role).toBeFalsy();
|
||||
settings: createQueryResolverStub<QueryToSettingsResolver>(
|
||||
() => settings
|
||||
),
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(variables => {
|
||||
expectAndFail(variables.role).toBeFalsy();
|
||||
return communityUsers;
|
||||
}),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: createQueryResolverStub<QueryToViewerResolver>(
|
||||
() => users.admins[0]
|
||||
),
|
||||
...resolver.Query,
|
||||
},
|
||||
};
|
||||
@@ -61,7 +74,9 @@ it("renders community", async () => {
|
||||
it("renders empty community", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: sinon.stub().returns(emptyCommunityUsers),
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(
|
||||
() => emptyCommunityUsers
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
@@ -70,13 +85,16 @@ it("renders empty community", async () => {
|
||||
it("filter by role", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createSinonStub(
|
||||
s => s.onFirstCall().returns(communityUsers),
|
||||
s =>
|
||||
s.onSecondCall().callsFake((_, data) => {
|
||||
expectAndFail(data.role).toBe(GQLUSER_ROLE.COMMENTER);
|
||||
return emptyCommunityUsers;
|
||||
})
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(
|
||||
(variables, callCount) => {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
return communityUsers;
|
||||
default:
|
||||
expectAndFail(variables.role).toBe(GQLUSER_ROLE.COMMENTER);
|
||||
return emptyCommunityUsers;
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -98,26 +116,27 @@ it("filter by role", async () => {
|
||||
});
|
||||
|
||||
it("can't change viewer role", async () => {
|
||||
const viewer = users[0];
|
||||
const viewer = users.admins[0];
|
||||
const { container } = await createTestRenderer();
|
||||
|
||||
const viewerRow = within(container).getByText(viewer.username, {
|
||||
const viewerRow = within(container).getByText(viewer.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
expect(() => within(viewerRow).getByLabelText("Change role")).toThrow();
|
||||
});
|
||||
|
||||
it("change user role", async () => {
|
||||
const user = users[1];
|
||||
const updateUserRole = sinon.stub().callsFake((_: any, data: any) => {
|
||||
expectAndFail(data.input).toMatchObject({
|
||||
const user = users.commenters[0];
|
||||
const updateUserRole = createMutationResolverStub<
|
||||
typeof UpdateUserRoleMutation
|
||||
>(variables => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
role: GQLUSER_ROLE.STAFF,
|
||||
});
|
||||
const userRecord = merge({}, user, { role: data.input.role });
|
||||
const userRecord = merge({}, user, { role: variables.role });
|
||||
return {
|
||||
user: userRecord,
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -125,7 +144,7 @@ it("change user role", async () => {
|
||||
Mutation: { updateUserRole },
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username, {
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
@@ -150,10 +169,10 @@ it("change user role", async () => {
|
||||
});
|
||||
|
||||
it("can't change role as a moderator", async () => {
|
||||
const viewer = users[1];
|
||||
const viewer = users.moderators[0];
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns(viewer),
|
||||
viewer: createQueryResolverStub<QueryToViewerResolver>(() => viewer),
|
||||
},
|
||||
});
|
||||
expect(() => within(container).getByLabelText("Change role")).toThrow();
|
||||
@@ -162,20 +181,38 @@ it("can't change role as a moderator", async () => {
|
||||
it("load more", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createSinonStub(
|
||||
s =>
|
||||
s.onFirstCall().returns({
|
||||
edges: [
|
||||
{ node: users[0], cursor: users[0].createdAt },
|
||||
{ node: users[1], cursor: users[1].createdAt },
|
||||
],
|
||||
pageInfo: { endCursor: users[1].createdAt, hasNextPage: true },
|
||||
}),
|
||||
s =>
|
||||
s.onSecondCall().returns({
|
||||
edges: [{ node: users[2], cursor: users[2].createdAt }],
|
||||
pageInfo: { endCursor: users[2].createdAt, hasNextPage: false },
|
||||
})
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(
|
||||
(variables, callCount) => {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
return {
|
||||
edges: [
|
||||
{ node: users.admins[0], cursor: users.admins[0].createdAt },
|
||||
{
|
||||
node: users.commenters[0],
|
||||
cursor: users.commenters[0].createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: users.commenters[0].createdAt,
|
||||
hasNextPage: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
edges: [
|
||||
{
|
||||
node: users.commenters[1],
|
||||
cursor: users.commenters[1].createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: users.commenters[1].createdAt,
|
||||
hasNextPage: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -188,19 +225,22 @@ it("load more", async () => {
|
||||
await waitUntilThrow(() => within(container).getByText("Load More"));
|
||||
|
||||
// Make sure third user was added.
|
||||
within(container).getByText(users[2].username);
|
||||
within(container).getByText(users.commenters[1].username!);
|
||||
});
|
||||
|
||||
it("filter by search", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createSinonStub(
|
||||
s => s.onFirstCall().returns(communityUsers),
|
||||
s =>
|
||||
s.onSecondCall().callsFake((_, data) => {
|
||||
expectAndFail(data.query).toBe("search");
|
||||
return emptyCommunityUsers;
|
||||
})
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(
|
||||
(variables, callCount) => {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
return communityUsers;
|
||||
default:
|
||||
expectAndFail(variables.query).toBe("search");
|
||||
return emptyCommunityUsers;
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -221,3 +261,170 @@ it("filter by search", async () => {
|
||||
within(container).getByText("could not find anyone", { exact: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("filter by status", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(
|
||||
(variables, callCount) => {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
return communityUsers;
|
||||
default:
|
||||
expectAndFail(variables.status).toBe("BANNED");
|
||||
return emptyCommunityUsers;
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const statusField = within(container).getByLabelText(
|
||||
"Search by user status",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
);
|
||||
const bannedOption = within(statusField).getByText("Banned");
|
||||
|
||||
TestRenderer.act(() => {
|
||||
statusField.props.onChange({
|
||||
target: { value: bannedOption.props.value.toString() },
|
||||
});
|
||||
// TODO: Fix act warnings until await Promise.resolve();
|
||||
// or whatever comes out at https://github.com/facebook/react/issues/14769
|
||||
});
|
||||
|
||||
await waitForElement(() =>
|
||||
within(container).getByText("We could not find anyone", { exact: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("can't change staff, moderator and admin status", async () => {
|
||||
const { container } = await createTestRenderer();
|
||||
["Admin", "Moderator", "Staff"].forEach(role => {
|
||||
const viewerRow = within(container).getByText(role, {
|
||||
selector: "tr",
|
||||
});
|
||||
expect(() =>
|
||||
within(viewerRow).getByLabelText("Change user status")
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("ban user", async () => {
|
||||
const user = users.commenters[0];
|
||||
const banUser = createMutationResolverStub<typeof BanUserMutation>(
|
||||
variables => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
});
|
||||
const userRecord = merge({}, user, {
|
||||
status: {
|
||||
current: user.status.current.concat(GQLUSER_STATUS.BANNED),
|
||||
banned: { active: true },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const { container, testRenderer } = await createTestRenderer({
|
||||
Mutation: { banUser },
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Ban User")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const modal = within(testRenderer.root).getByLabelText(
|
||||
"Are you sure you want to ban",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(within(modal).toJSON()).toMatchSnapshot();
|
||||
|
||||
within(modal)
|
||||
.getByText("Ban User")
|
||||
.props.onClick();
|
||||
within(userRow).getByText("Banned");
|
||||
expect(banUser.called).toBe(true);
|
||||
});
|
||||
|
||||
it("remove user ban", async () => {
|
||||
const user = users.bannedCommenter;
|
||||
const removeUserBan = createMutationResolverStub<
|
||||
typeof RemoveUserBanMutation
|
||||
>(variables => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
});
|
||||
const userRecord = merge({}, user, {
|
||||
status: {
|
||||
current: user.status.current.filter(s => s !== GQLUSER_STATUS.BANNED),
|
||||
banned: { active: false },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
});
|
||||
|
||||
const { container } = await createTestRenderer({
|
||||
Mutation: { removeUserBan },
|
||||
Query: {
|
||||
users: createQueryResolverStub<QueryToUsersResolver>(() => ({
|
||||
edges: [
|
||||
{
|
||||
node: user,
|
||||
cursor: user.createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Remove Ban")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
within(userRow).getByText("Active");
|
||||
expect(removeUserBan.called).toBe(true);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -28,7 +28,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
.returns(
|
||||
merge({}, settingsWithEmptyAuth, get(resolver, "Query.settings"))
|
||||
),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -29,7 +29,7 @@ const createTestRenderer = async (
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -47,7 +47,7 @@ it("denies access to moderators", async () => {
|
||||
for (const r of deniedRoles) {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns({ ...users[0], role: r }),
|
||||
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
|
||||
},
|
||||
});
|
||||
await waitForElement(() =>
|
||||
@@ -61,7 +61,7 @@ it("allows access to admins", async () => {
|
||||
for (const r of deniedRoles) {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns({ ...users[0], role: r }),
|
||||
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
|
||||
},
|
||||
});
|
||||
await waitForElement(() =>
|
||||
|
||||
@@ -24,7 +24,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -62,7 +62,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
Query: {
|
||||
...resolver.Query,
|
||||
viewer: sinon.stub().returns({
|
||||
...users[0],
|
||||
...users.admins[0],
|
||||
commentModerationActionHistory,
|
||||
}),
|
||||
settings: sinon
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { merge } from "lodash";
|
||||
import {
|
||||
GQLComment,
|
||||
GQLCOMMENT_STATUS,
|
||||
GQLCommentModerationAction,
|
||||
GQLCommentsConnection,
|
||||
GQLMODERATION_MODE,
|
||||
GQLModerationQueues,
|
||||
GQLSettings,
|
||||
GQLStoriesConnection,
|
||||
GQLStory,
|
||||
GQLSTORY_STATUS,
|
||||
GQLUser,
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_STATUS,
|
||||
GQLUsersConnection,
|
||||
} from "talk-framework/schema";
|
||||
import { createFixture, createFixtures } from "talk-framework/testHelpers";
|
||||
|
||||
import { GQLStory, GQLSTORY_STATUS, GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
export const settings = {
|
||||
export const settings = createFixture<GQLSettings>({
|
||||
id: "settings",
|
||||
moderation: "POST",
|
||||
moderation: GQLMODERATION_MODE.POST,
|
||||
premodLinksEnable: false,
|
||||
wordList: {
|
||||
suspect: ["idiot", "stupid"],
|
||||
@@ -24,7 +38,7 @@ export const settings = {
|
||||
timeout: 604800,
|
||||
message: "Comments are closed on this story.",
|
||||
},
|
||||
customCSSURL: null,
|
||||
customCSSURL: "",
|
||||
domains: ["localhost:8080"],
|
||||
editCommentWindowLength: 30000,
|
||||
communityGuidelines: {
|
||||
@@ -61,7 +75,7 @@ export const settings = {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
key: null,
|
||||
key: "",
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
google: {
|
||||
@@ -101,71 +115,73 @@ export const settings = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const settingsWithEmptyAuth = {
|
||||
...settings,
|
||||
id: "settings",
|
||||
auth: {
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: true,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
export const settingsWithEmptyAuth = createFixture<GQLSettings>(
|
||||
{
|
||||
id: "settings",
|
||||
auth: {
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: true,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
sso: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
sso: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
key: "",
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
key: null,
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
google: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
google: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/google/callback",
|
||||
redirectURL: "http://localhost/google",
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/google/callback",
|
||||
redirectURL: "http://localhost/google",
|
||||
},
|
||||
facebook: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
facebook: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/facebook/callback",
|
||||
redirectURL: "http://localhost/facebook",
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/facebook/callback",
|
||||
redirectURL: "http://localhost/facebook",
|
||||
},
|
||||
oidc: {
|
||||
enabled: false,
|
||||
allowRegistration: false,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
oidc: {
|
||||
enabled: false,
|
||||
allowRegistration: false,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
name: "",
|
||||
callbackURL: "http://localhost/oidc/callback",
|
||||
redirectURL: "http://localhost/oidc",
|
||||
},
|
||||
name: "",
|
||||
callbackURL: "http://localhost/oidc/callback",
|
||||
redirectURL: "http://localhost/oidc",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
settings
|
||||
);
|
||||
|
||||
export const moderationActions = [
|
||||
export const moderationActions = createFixtures<GQLCommentModerationAction>([
|
||||
{
|
||||
id: "07e8f815-e165-4b5d-b438-7163415c8cf7",
|
||||
revision: {
|
||||
@@ -179,7 +195,7 @@ export const moderationActions = [
|
||||
},
|
||||
},
|
||||
createdAt: "2018-11-29T16:01:51.897Z",
|
||||
status: "ACCEPTED",
|
||||
status: GQLCOMMENT_STATUS.ACCEPTED,
|
||||
__typename: "CommentModerationAction",
|
||||
},
|
||||
{
|
||||
@@ -195,7 +211,7 @@ export const moderationActions = [
|
||||
},
|
||||
},
|
||||
createdAt: "2018-11-29T16:01:45.644Z",
|
||||
status: "REJECTED",
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
__typename: "CommentModerationAction",
|
||||
},
|
||||
{
|
||||
@@ -211,7 +227,7 @@ export const moderationActions = [
|
||||
},
|
||||
},
|
||||
createdAt: "2018-11-29T16:01:42.060Z",
|
||||
status: "ACCEPTED",
|
||||
status: GQLCOMMENT_STATUS.ACCEPTED,
|
||||
__typename: "CommentModerationAction",
|
||||
},
|
||||
{
|
||||
@@ -227,7 +243,7 @@ export const moderationActions = [
|
||||
},
|
||||
},
|
||||
createdAt: "2018-11-29T16:01:34.539Z",
|
||||
status: "REJECTED",
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
__typename: "CommentModerationAction",
|
||||
},
|
||||
{
|
||||
@@ -243,45 +259,105 @@ export const moderationActions = [
|
||||
},
|
||||
},
|
||||
createdAt: "2018-11-29T16:01:30.648Z",
|
||||
status: "ACCEPTED",
|
||||
status: GQLCOMMENT_STATUS.ACCEPTED,
|
||||
__typename: "CommentModerationAction",
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
export const baseUser = {
|
||||
export const baseUser = createFixture<GQLUser>({
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
status: {
|
||||
current: [],
|
||||
ban: {
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
suspension: {
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const users = {
|
||||
admins: createFixtures<GQLUser>(
|
||||
[
|
||||
{
|
||||
id: "user-admin-0",
|
||||
username: "Markus",
|
||||
email: "markus@test.com",
|
||||
role: GQLUSER_ROLE.ADMIN,
|
||||
},
|
||||
],
|
||||
baseUser
|
||||
),
|
||||
moderators: createFixtures<GQLUser>(
|
||||
[
|
||||
{
|
||||
id: "user-moderator-0",
|
||||
username: "Lukas",
|
||||
email: "lukas@test.com",
|
||||
role: GQLUSER_ROLE.MODERATOR,
|
||||
},
|
||||
],
|
||||
baseUser
|
||||
),
|
||||
staff: createFixtures<GQLUser>(
|
||||
[
|
||||
{
|
||||
id: "user-staff-0",
|
||||
username: "Huy",
|
||||
email: "huy@test.com",
|
||||
role: GQLUSER_ROLE.STAFF,
|
||||
},
|
||||
],
|
||||
baseUser
|
||||
),
|
||||
commenters: createFixtures<GQLUser>(
|
||||
[
|
||||
{
|
||||
id: "user-commenter-0",
|
||||
username: "Isabelle",
|
||||
email: "isabelle@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
{
|
||||
id: "user-commenter-1",
|
||||
username: "Ngoc",
|
||||
email: "ngoc@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
{
|
||||
id: "user-commenter-2",
|
||||
username: "Max",
|
||||
email: "max@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
],
|
||||
baseUser
|
||||
),
|
||||
bannedCommenter: createFixture<GQLUser>(
|
||||
{
|
||||
id: "user-banned-0",
|
||||
username: "Ingrid",
|
||||
email: "ingrid@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.BANNED],
|
||||
ban: { active: true },
|
||||
suspension: { active: true },
|
||||
},
|
||||
},
|
||||
baseUser
|
||||
),
|
||||
};
|
||||
|
||||
export const users = [
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-0",
|
||||
username: "Markus",
|
||||
email: "markus@test.com",
|
||||
role: GQLUSER_ROLE.ADMIN,
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-1",
|
||||
username: "Lukas",
|
||||
email: "lukas@test.com",
|
||||
role: GQLUSER_ROLE.MODERATOR,
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-2",
|
||||
username: "Isabelle",
|
||||
email: "isabelle@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
];
|
||||
|
||||
export const baseComment = {
|
||||
author: users[0],
|
||||
export const baseComment = createFixture<GQLComment>({
|
||||
author: users.commenters[0],
|
||||
body: "Comment Body",
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
status: "NONE",
|
||||
status: GQLCOMMENT_STATUS.NONE,
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
@@ -296,68 +372,71 @@ export const baseComment = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const reportedComments = [
|
||||
merge({}, baseComment, {
|
||||
id: "comment-0",
|
||||
author: users[0],
|
||||
revision: {
|
||||
id: "comment-0-revision-0",
|
||||
},
|
||||
permalink: "http://localhost/comment/0",
|
||||
body:
|
||||
"This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 2,
|
||||
export const reportedComments = createFixtures<GQLComment>(
|
||||
[
|
||||
{
|
||||
id: "comment-0",
|
||||
author: users.commenters[0],
|
||||
revision: {
|
||||
id: "comment-0-revision-0",
|
||||
},
|
||||
permalink: "http://localhost/comment/0",
|
||||
body:
|
||||
"This is the last random sentence I will be writing and I am going to stop mid-sent",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
merge({}, baseComment, {
|
||||
id: "comment-1",
|
||||
revision: {
|
||||
id: "comment-1-revision-1",
|
||||
},
|
||||
permalink: "http://localhost/comment/1",
|
||||
author: users[1],
|
||||
body: "Don't fool with me",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_OFFENSIVE: 3,
|
||||
{
|
||||
id: "comment-1",
|
||||
revision: {
|
||||
id: "comment-1-revision-1",
|
||||
},
|
||||
permalink: "http://localhost/comment/1",
|
||||
author: users.commenters[1],
|
||||
body: "Don't fool with me",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_OFFENSIVE: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
merge({}, baseComment, {
|
||||
id: "comment-2",
|
||||
revision: {
|
||||
id: "comment-2-revision-2",
|
||||
},
|
||||
permalink: "http://localhost/comment/2",
|
||||
status: "PREMOD",
|
||||
author: users[2],
|
||||
body: "I think I deserve better",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 1,
|
||||
{
|
||||
id: "comment-2",
|
||||
revision: {
|
||||
id: "comment-2-revision-2",
|
||||
},
|
||||
permalink: "http://localhost/comment/2",
|
||||
status: GQLCOMMENT_STATUS.PREMOD,
|
||||
author: users.commenters[2],
|
||||
body: "I think I deserve better",
|
||||
actionCounts: {
|
||||
flag: {
|
||||
reasons: {
|
||||
COMMENT_REPORTED_SPAM: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
],
|
||||
baseComment
|
||||
);
|
||||
|
||||
export const rejectedComments = reportedComments.map(c => ({
|
||||
export const rejectedComments = reportedComments.map<GQLComment>(c => ({
|
||||
...c,
|
||||
status: "REJECTED",
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
}));
|
||||
|
||||
export const emptyModerationQueues = {
|
||||
export const emptyModerationQueues = createFixture<GQLModerationQueues>({
|
||||
reported: {
|
||||
id: "reported",
|
||||
count: 0,
|
||||
@@ -373,27 +452,29 @@ export const emptyModerationQueues = {
|
||||
count: 0,
|
||||
comments: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const emptyRejectedComments = {
|
||||
export const emptyRejectedComments = createFixture<GQLCommentsConnection>({
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
});
|
||||
|
||||
export const communityUsers = {
|
||||
export const communityUsers = createFixture<GQLUsersConnection>({
|
||||
edges: [
|
||||
{ node: users[0], cursor: users[0].createdAt },
|
||||
{ node: users[1], cursor: users[1].createdAt },
|
||||
{ node: users.admins[0], cursor: users.admins[0].createdAt },
|
||||
{ node: users.moderators[0], cursor: users.moderators[0].createdAt },
|
||||
{ node: users.staff[0], cursor: users.staff[0].createdAt },
|
||||
{ node: users.commenters[0], cursor: users.commenters[0].createdAt },
|
||||
],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
});
|
||||
|
||||
export const emptyCommunityUsers = {
|
||||
export const emptyCommunityUsers = createFixture<GQLUsersConnection>({
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
});
|
||||
|
||||
export const stories: Array<Partial<GQLStory>> = [
|
||||
export const stories = createFixtures<GQLStory>([
|
||||
{
|
||||
id: "story-1",
|
||||
closedAt: null,
|
||||
@@ -430,17 +511,17 @@ export const stories: Array<Partial<GQLStory>> = [
|
||||
publishedAt: "2018-11-29T16:01:51.897Z",
|
||||
},
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
export const storyConnection = {
|
||||
export const storyConnection = createFixture<GQLStoriesConnection>({
|
||||
edges: [
|
||||
{ node: stories[0], cursor: stories[0].createdAt },
|
||||
{ node: stories[1], cursor: stories[1].createdAt },
|
||||
],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
});
|
||||
|
||||
export const emptyStories = {
|
||||
export const emptyStories = createFixture<GQLStoriesConnection>({
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -157,7 +157,7 @@ exports[`rejected queue accepts comment in rejected queue: dangling 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -310,7 +310,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -450,7 +450,7 @@ exports[`rejected queue renders rejected queue with comments 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Lukas
|
||||
Ngoc
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -597,7 +597,7 @@ exports[`rejected queue renders rejected queue with comments and load more 1`] =
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Isabelle
|
||||
Max
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -766,7 +766,7 @@ exports[`reported queue accepts comment in reported queue: dangling 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -922,7 +922,7 @@ exports[`reported queue rejects comment in reported queue: dangling 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1090,7 +1090,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1230,7 +1230,7 @@ exports[`reported queue renders reported queue with comments 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Lukas
|
||||
Ngoc
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1376,7 +1376,7 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Isabelle
|
||||
Max
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1537,7 +1537,7 @@ exports[`single comment view accepts single comment 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1680,7 +1680,7 @@ exports[`single comment view rejects single comment 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
@@ -1858,7 +1858,7 @@ exports[`single comment view renders single comment view 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Typography-colorTextPrimary ModerateCard-username Username-root"
|
||||
>
|
||||
Markus
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
|
||||
@@ -44,7 +44,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
comments:
|
||||
get(resolver, "Query.comments") ||
|
||||
sinon.stub().returns(emptyRejectedComments),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -36,7 +36,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
expectAndFail(data.status).toBeFalsy();
|
||||
return storyConnection;
|
||||
}),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
viewer: sinon.stub().returns(users.admins[0]),
|
||||
...resolver.Query,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,21 +2,23 @@ import { RouteProps } from "found";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { RestrictedContainer_viewer as ViewerData } from "talk-admin/__generated__/RestrictedContainer_viewer.graphql";
|
||||
import { SetRedirectPathMutation } from "talk-admin/mutations";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
graphql,
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import Restricted from "../components/Restricted";
|
||||
|
||||
interface Props {
|
||||
viewer: ViewerData;
|
||||
error?: Error | null;
|
||||
signOut: SignOutMutation;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
setRedirectPath: MutationProp<typeof SetRedirectPathMutation>;
|
||||
}
|
||||
|
||||
class RestrictedContainer extends Component<Props> {
|
||||
@@ -51,6 +53,10 @@ const enhanced = withFragmentContainer<Props>({
|
||||
username
|
||||
}
|
||||
`,
|
||||
})(withSetRedirectPathMutation(withSignOutMutation(RestrictedContainer)));
|
||||
})(
|
||||
withMutation(SetRedirectPathMutation)(
|
||||
withSignOutMutation(RestrictedContainer)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -14,6 +14,14 @@ interface Props {
|
||||
localeBundles: TalkContext["localeBundles"];
|
||||
}
|
||||
|
||||
interface InjectedProps {
|
||||
getMessage: GetMessage;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
localeBundles: TalkContext["localeBundles"];
|
||||
}
|
||||
|
||||
/**
|
||||
* withGetMessage provides a property `getMessage: (id: string) => string`
|
||||
* that'll provide a translated string associated with `id`.
|
||||
@@ -22,13 +30,17 @@ const withGetMessage: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
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);
|
||||
};
|
||||
public render() {
|
||||
const { localeBundles: _, ...rest } = this.props;
|
||||
return <BaseComponent {...rest} getMessage={this.getMessage} />;
|
||||
return <Workaround {...rest} getMessage={this.getMessage} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ const withInView: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
|
||||
|
||||
class WithInView extends React.Component<Props, State> {
|
||||
private unobserve: (() => void) | null = null;
|
||||
|
||||
@@ -62,7 +66,7 @@ const withInView: DefaultingInferableComponentEnhancer<
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<BaseComponent
|
||||
<Workaround
|
||||
{...this.props}
|
||||
inView={this.state.inView}
|
||||
intersectionRef={this.changeRef}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { commitMutation } from "react-relay";
|
||||
import { Environment, MutationConfig, OperationBase } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
import { DeepPartial, Omit } from "talk-framework/types";
|
||||
|
||||
import extractPayload from "./extractPayload";
|
||||
|
||||
@@ -11,8 +11,8 @@ import extractPayload from "./extractPayload";
|
||||
*/
|
||||
export type MutationPromiseConfig<T extends OperationBase> = Omit<
|
||||
MutationConfig<T>,
|
||||
"onCompleted" | "onError"
|
||||
>;
|
||||
"onCompleted" | "onError" | "optimisticResponse"
|
||||
> & { optimisticResponse?: DeepPartial<T["response"]> };
|
||||
|
||||
/**
|
||||
* Normalizes response and error from `commitMutationPromise`.
|
||||
|
||||
@@ -6,24 +6,9 @@ import {
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { TalkContext, withContext } from "../bootstrap";
|
||||
|
||||
export type MutationInput<
|
||||
T extends { variables: { input: { clientMutationId: string } } }
|
||||
> = Omit<T["variables"]["input"], "clientMutationId">;
|
||||
|
||||
export type MutationResponse<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string
|
||||
> = Exclude<T["response"][U], null>;
|
||||
|
||||
export type MutationResponsePromise<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string
|
||||
> = Promise<MutationResponse<T, U>>;
|
||||
|
||||
/**
|
||||
* createMutationContainer creates a HOC that
|
||||
* injects a property with the name specified in `propName`
|
||||
|
||||
@@ -5,12 +5,16 @@ export { default as withLocalStateContainer } from "./withLocalStateContainer";
|
||||
export * from "./withLocalStateContainer";
|
||||
export { default as QueryRenderer } from "./QueryRenderer";
|
||||
export * from "./QueryRenderer";
|
||||
export { default as createMutationContainer } from "./createMutationContainer";
|
||||
export {
|
||||
default as createMutationContainer,
|
||||
createMutation,
|
||||
useMutation,
|
||||
withMutation,
|
||||
MutationInput,
|
||||
MutationResponse,
|
||||
MutationResponsePromise,
|
||||
} from "./createMutationContainer";
|
||||
MutationProp,
|
||||
} from "./mutation";
|
||||
export { default as createFetchContainer } from "./createFetchContainer";
|
||||
export { default as createAndRetain } from "./createAndRetain";
|
||||
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
|
||||
@@ -26,3 +30,4 @@ export { initLocalBaseState, setAccessTokenInLocalState } from "./localState";
|
||||
export { default as fetchQuery } from "./fetchQuery";
|
||||
export { default as useRefetch } from "./useRefetch";
|
||||
export { default as useLoadMore } from "./useLoadMore";
|
||||
export { default as lookup } from "./lookup";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Environment, RelayInMemoryRecordSource } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* RecordSourceProxy has the same shape as the underlying Schema Type, but
|
||||
* makes all fields optional and readonly.
|
||||
*/
|
||||
type RecordSourceProxy<T> = T extends object
|
||||
? {
|
||||
readonly [P in keyof T]?: T[P] extends Array<infer U>
|
||||
? ReadonlyArray<RecordSourceProxy<U>>
|
||||
: T[P] extends ReadonlyArray<infer V>
|
||||
? ReadonlyArray<RecordSourceProxy<V>>
|
||||
: RecordSourceProxy<T[P]>
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* createProxy returns a proxy for `recordSource`, that automatically
|
||||
* resolves references to other record sources.
|
||||
*/
|
||||
const createProxy = <T = any>(
|
||||
environment: Environment,
|
||||
recordSource: RelayInMemoryRecordSource
|
||||
) => {
|
||||
const proxy: ProxyHandler<any> = {
|
||||
get(_, prop) {
|
||||
if ((recordSource as any)[prop].__ref) {
|
||||
return lookup(environment, (recordSource as any)[prop].__ref);
|
||||
}
|
||||
return (recordSource as any)[prop];
|
||||
},
|
||||
};
|
||||
return new Proxy({}, proxy) as RecordSourceProxy<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup the Relay Cache with given object id. Returns a `RecordSourceProxy``
|
||||
* for easy traversing through the Relay Cache.
|
||||
*/
|
||||
export default function lookup<T = any>(environment: Environment, id: string) {
|
||||
const recordSource = environment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(id);
|
||||
if (!recordSource) {
|
||||
return null;
|
||||
}
|
||||
return createProxy<T>(environment, recordSource);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
compose,
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { TalkContext, useTalkContext, withContext } from "../bootstrap";
|
||||
|
||||
export interface Mutation<N, I, R> {
|
||||
name: N;
|
||||
commit: (environment: Environment, input: I, context: TalkContext) => R;
|
||||
}
|
||||
|
||||
export type MutationInput<
|
||||
T extends { variables: { input: { clientMutationId: string } } }
|
||||
> = Omit<T["variables"]["input"], "clientMutationId">;
|
||||
|
||||
export type MutationResponse<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string | number | symbol
|
||||
> = Exclude<T["response"][U], null>;
|
||||
|
||||
export type MutationResponsePromise<
|
||||
T extends { response: { [P in U]: any } },
|
||||
U extends string | number | symbol
|
||||
> = Promise<MutationResponse<T, U>>;
|
||||
|
||||
export type MutationProp<
|
||||
T extends Mutation<any, any, any>
|
||||
> = T extends Mutation<any, infer I, infer R>
|
||||
? Parameters<T["commit"]>[1] extends undefined
|
||||
? () => R
|
||||
: keyof Parameters<T["commit"]>[1] extends never ? () => R : (input: I) => R
|
||||
: never;
|
||||
|
||||
type RemoveClientMutationID<T> = T extends Promise<infer U>
|
||||
? Promise<
|
||||
U extends { clientMutationId: any } ? Omit<U, "clientMutationId"> : U
|
||||
>
|
||||
: T extends { clientMutationId: any } ? Omit<T, "clientMutationId"> : T;
|
||||
|
||||
export function createMutation<N extends string, I, R>(
|
||||
name: N,
|
||||
commit: (environment: Environment, input: I, context: TalkContext) => R
|
||||
// (cvle) We remove `clientMutationId` from the response, so we don't use it inside our app.
|
||||
// It is a Relay implementation detail that is pending for removal.
|
||||
// https://github.com/facebook/relay/pull/2349
|
||||
): Mutation<N, I, RemoveClientMutationID<R>> {
|
||||
return {
|
||||
name,
|
||||
commit,
|
||||
} as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* useMutation is a React Hook that
|
||||
* returns a callback to call the mutation.
|
||||
*/
|
||||
export function useMutation<I, R>(
|
||||
mutation: Mutation<any, I, R>
|
||||
): MutationProp<typeof mutation> {
|
||||
const context = useTalkContext();
|
||||
return useCallback<MutationProp<typeof mutation>>(
|
||||
((input: I) => {
|
||||
context.eventEmitter.emit(`mutation.${mutation.name}`, input);
|
||||
return mutation.commit(context.relayEnvironment, input, context);
|
||||
}) as any,
|
||||
[context]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* withMutation creates a HOC that injects the mutation as
|
||||
* a property.
|
||||
*/
|
||||
export function withMutation<N extends string, I, R>(
|
||||
mutation: Mutation<N, I, R>
|
||||
): InferableComponentEnhancer<{ [P in N]: MutationProp<typeof mutation> }> {
|
||||
return compose(
|
||||
withContext(context => ({ context })),
|
||||
hoistStatics((BaseComponent: React.ComponentType<any>) => {
|
||||
class WithMutation extends React.Component<{
|
||||
context: TalkContext;
|
||||
}> {
|
||||
public static displayName = wrapDisplayName(
|
||||
BaseComponent,
|
||||
"withMutation"
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
this.props.context.eventEmitter.emit(
|
||||
`mutation.${mutation.name}`,
|
||||
input
|
||||
);
|
||||
return mutation.commit(
|
||||
this.props.context.relayEnvironment,
|
||||
input,
|
||||
this.props.context
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { context: _, ...rest } = this.props;
|
||||
const inject = {
|
||||
[mutation.name]: this.commit,
|
||||
};
|
||||
return <BaseComponent {...rest} {...inject} />;
|
||||
}
|
||||
}
|
||||
return WithMutation as React.ComponentType<any>;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
GQLSTORY_STATUS,
|
||||
GQLUSER_AUTH_CONDITIONS,
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_STATUS,
|
||||
} from "./__generated__/types";
|
||||
|
||||
export type GQLUSER_ROLE_RL = RelayEnumLiteral<typeof GQLUSER_ROLE>;
|
||||
export type GQLUSER_STATUS_RL = RelayEnumLiteral<typeof GQLUSER_STATUS>;
|
||||
export type GQLCOMMENT_FLAG_DETECTED_REASON_RL = RelayEnumLiteral<
|
||||
typeof GQLCOMMENT_FLAG_DETECTED_REASON
|
||||
>;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { merge } from "lodash";
|
||||
|
||||
/**
|
||||
* Fixture prepares schema type to be used in fixtures.
|
||||
* It adds an optional `__typename` to the schema type and
|
||||
* marks fields as optional.
|
||||
*/
|
||||
export type Fixture<T> = T extends object
|
||||
? {
|
||||
// (cvle): We don't use & { __typename?: string } because for some reason
|
||||
// typescript would allow field names that are not defined!
|
||||
[P in keyof T | "__typename"]?: P extends keyof T
|
||||
? T[P] extends Array<infer U>
|
||||
? Array<Fixture<U>>
|
||||
: T[P] extends ReadonlyArray<infer V>
|
||||
? ReadonlyArray<Fixture<V>>
|
||||
: Fixture<T[P]>
|
||||
: string
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* createFixture lets you input the data of a schema object as deep partial
|
||||
* including it's `__typename`, merged it with `base` and return it as the
|
||||
* schema object's type. In the future the result could be trimmed down
|
||||
* to only include fields that exists in `data` and `base` though to
|
||||
* type this it seems we need partial generic inferation support.
|
||||
*/
|
||||
export default function createFixture<T>(data: Fixture<T>, base?: T): T {
|
||||
if (base) {
|
||||
return merge({}, base, data);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import createFixture, { Fixture } from "./createFixture";
|
||||
|
||||
/**
|
||||
* createFixtures lets you input an array of data of a schema object as deep partial
|
||||
* including it's `__typename`, merged it with `base` and return it as any array of the
|
||||
* schema object's type. In the future the result could be trimmed down
|
||||
* to only include fields that exists in `data` and `base` though to
|
||||
* type this it seems we need partial generic inferation support.
|
||||
*/
|
||||
export default function createFixtures<T>(
|
||||
data: Array<Fixture<T>>,
|
||||
base?: T
|
||||
): T[] {
|
||||
return data.map(d => createFixture(d, base)) as T[];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import sinon from "sinon";
|
||||
import { Mutation } from "talk-framework/lib/relay/mutation";
|
||||
|
||||
export default function createMutationResolverStub<
|
||||
T extends Mutation<any, any, any>
|
||||
>(
|
||||
callback: (
|
||||
variables: T extends Mutation<any, infer I, any> ? I : never,
|
||||
callCount: number
|
||||
) => T extends Mutation<any, any, infer R>
|
||||
? R extends Promise<infer U> ? U | R : R | Promise<R>
|
||||
: never
|
||||
) {
|
||||
let callCount = 0;
|
||||
const lastClientMutationIds: any[] = [];
|
||||
const resolver = async (_: any, data: any) => {
|
||||
const clientMutationId = data.input.clientMutationId;
|
||||
expectAndFail(clientMutationId).toBeTruthy();
|
||||
expectAndFail(lastClientMutationIds).not.toContain(clientMutationId);
|
||||
lastClientMutationIds.push(clientMutationId);
|
||||
const result = await callback(data.input, callCount++);
|
||||
expectAndFail(result.clientMutationId).toBeUndefined();
|
||||
result.clientMutationId = clientMutationId;
|
||||
return result;
|
||||
};
|
||||
return sinon.stub().callsFake(resolver);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
type Resolver<V, R> = (parent: any, args: V, context: any, info: any) => R;
|
||||
|
||||
export default function createQueryResolverStub<T extends Resolver<any, any>>(
|
||||
callback: (
|
||||
variables: T extends Resolver<infer V, any> ? V : never,
|
||||
callCount: number
|
||||
) => T extends Resolver<any, infer R>
|
||||
? R extends Promise<infer U> ? U | R : R | Promise<R>
|
||||
: never
|
||||
) {
|
||||
let callCount = 0;
|
||||
return sinon
|
||||
.stub()
|
||||
.callsFake((_: any, data: any) => callback(data, callCount++));
|
||||
}
|
||||
@@ -22,3 +22,9 @@ export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
|
||||
export { default as createAccessToken } from "./createAccessToken";
|
||||
export { default as findParentsWithType } from "./findParentsWithType";
|
||||
export { default as findParentWithType } from "./findParentWithType";
|
||||
export { default as createFixture } from "./createFixture";
|
||||
export { default as createFixtures } from "./createFixtures";
|
||||
export {
|
||||
default as createMutationResolverStub,
|
||||
} from "./createMutationResolverStub";
|
||||
export { default as createQueryResolverStub } from "./createQueryResolverStub";
|
||||
|
||||
@@ -20,10 +20,14 @@ export interface CloseButtonProps
|
||||
}
|
||||
|
||||
const CloseButton: StatelessComponent<CloseButtonProps> = props => {
|
||||
const { classes, forwardRef, ...rest } = props;
|
||||
const {
|
||||
classes: { icon: iconClassName, ...restClasses },
|
||||
forwardRef,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<BaseButton classes={classes} ref={forwardRef} {...rest}>
|
||||
<Icon className={classes.icon} size="md">
|
||||
<BaseButton classes={restClasses} ref={forwardRef} {...rest}>
|
||||
<Icon className={iconClassName} size="md">
|
||||
close
|
||||
</Icon>
|
||||
</BaseButton>
|
||||
|
||||
@@ -29,15 +29,14 @@ import styles from "./Modal.mdx.css";
|
||||
<Button variant="filled" color="primary" onClick={show}>
|
||||
Show Modal
|
||||
</Button>
|
||||
<Modal open={open} onClose={hide} aria-labeledby="modal-title">
|
||||
<Modal open={open} onClose={hide} aria-labelledby="modal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={hide} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Typography variant="header2" id="modal-title">
|
||||
Are you sure you want to ban <strong>ButFirstCoffee</strong>
|
||||
?
|
||||
Are you sure you want to ban <strong>ButFirstCoffee</strong>?
|
||||
</Typography>
|
||||
<Typography>
|
||||
Once banned, this user will no longer be able to comment,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
appearance: none;
|
||||
outline: none;
|
||||
color: var(--palette-text-primary);
|
||||
width: 100%;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
color: var(--palette-error-main);
|
||||
}
|
||||
|
||||
.colorWarning {
|
||||
color: var(--palette-warning-main);
|
||||
}
|
||||
|
||||
.colorSuccess {
|
||||
color: var(--palette-success-main);
|
||||
color: var(--palette-success-darkest);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ interface Props extends HTMLAttributes<any> {
|
||||
| "textPrimary"
|
||||
| "textSecondary"
|
||||
| "textLight"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "success";
|
||||
/**
|
||||
@@ -114,6 +115,7 @@ const Typography: StatelessComponent<Props> = props => {
|
||||
[classes.colorPrimary]: color === "primary",
|
||||
[classes.colorError]: color === "error",
|
||||
[classes.colorSuccess]: color === "success",
|
||||
[classes.colorWarning]: color === "warning",
|
||||
[classes.noWrap]: noWrap,
|
||||
[classes.gutterBottom]: gutterBottom,
|
||||
[classes.paragraph]: paragraph,
|
||||
|
||||
@@ -55,3 +55,4 @@ export {
|
||||
} from "./Dropdown";
|
||||
export { Table, TableBody, TableHead, TableRow, TableCell } from "./Table";
|
||||
export { default as Tag } from "./Tag";
|
||||
export { default as Modal } from "./Modal";
|
||||
|
||||
@@ -17,6 +17,10 @@ const withKeyboardFocus: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
|
||||
|
||||
class WithKeyboardFocus extends React.Component<any> {
|
||||
private lastMouseDownTime: number = 0;
|
||||
public state = {
|
||||
@@ -55,7 +59,7 @@ const withKeyboardFocus: DefaultingInferableComponentEnhancer<
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<BaseComponent
|
||||
<Workaround
|
||||
{...this.props}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
|
||||
@@ -18,6 +18,10 @@ const withMouseHover: DefaultingInferableComponentEnhancer<
|
||||
InjectedProps
|
||||
> = hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(BaseComponent: React.ComponentType<T>) => {
|
||||
// TODO: (cvle) This is a workaround for a typescript bug
|
||||
// https://github.com/Microsoft/TypeScript/issues/30762
|
||||
const Workaround = BaseComponent as React.ComponentType<InjectedProps>;
|
||||
|
||||
class WithMouseHover extends React.Component<InjectedProps> {
|
||||
private lastTouchEndTime = 0;
|
||||
public state = {
|
||||
@@ -50,7 +54,7 @@ const withMouseHover: DefaultingInferableComponentEnhancer<
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<BaseComponent
|
||||
<Workaround
|
||||
{...this.props}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
|
||||
@@ -44,6 +44,10 @@ const variables = {
|
||||
lighter: "#FC9795",
|
||||
lightest: "#FEF0EF",
|
||||
},
|
||||
/* Warning colors */
|
||||
warning: {
|
||||
main: "#DC8400",
|
||||
},
|
||||
/* Text colors */
|
||||
text: {
|
||||
primary: "#3B4A53",
|
||||
|
||||
@@ -198,4 +198,28 @@ export enum ERROR_CODES {
|
||||
* enabled.
|
||||
*/
|
||||
SPAM_COMMENT = "SPAM_COMMENT",
|
||||
|
||||
/**
|
||||
* USER_ALREADY_SUSPENDED is returned when a moderator or administrator
|
||||
* attempts to suspend a user that already has an active suspension.
|
||||
*/
|
||||
USER_ALREADY_SUSPENDED = "USER_ALREADY_SUSPENDED",
|
||||
|
||||
/**
|
||||
* USER_ALREADY_BANNED is returned when a moderator or administrator attempts
|
||||
* to ban a user that is already banned.
|
||||
*/
|
||||
USER_ALREADY_BANNED = "USER_ALREADY_BANNED",
|
||||
|
||||
/**
|
||||
* USER_SUSPENDED is returned when the user attempts to perform an action that
|
||||
* is not permitted if they are suspended.
|
||||
*/
|
||||
USER_SUSPENDED = "USER_SUSPENDED",
|
||||
|
||||
/**
|
||||
* USER_BANNED is returned when the user attempts to perform an action that
|
||||
* is not permitted if they are banned.
|
||||
*/
|
||||
USER_BANNED = "USER_BANNED",
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ export type Promiseable<T> = Promise<T> | T;
|
||||
/**
|
||||
* Like Partial, but recurses down the object marking each field as Partial.
|
||||
*/
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T[P] extends ReadonlyArray<infer V>
|
||||
? ReadonlyArray<DeepPartial<V>>
|
||||
: DeepPartial<T[P]>
|
||||
};
|
||||
export type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: T[P] extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T[P] extends ReadonlyArray<infer V>
|
||||
? ReadonlyArray<DeepPartial<V>>
|
||||
: DeepPartial<T[P]>
|
||||
}
|
||||
: T;
|
||||
|
||||
@@ -38,6 +38,7 @@ export const signupHandler = ({
|
||||
|
||||
// Tenant is guaranteed at this point.
|
||||
const tenant = req.talk!.tenant!;
|
||||
const now = req.talk!.now;
|
||||
|
||||
// Check to ensure that the local integration has been enabled.
|
||||
if (!tenant.auth.integrations.local.enabled) {
|
||||
@@ -65,14 +66,19 @@ export const signupHandler = ({
|
||||
};
|
||||
|
||||
// Create the new user.
|
||||
const user = await insert(mongo, tenant, {
|
||||
email,
|
||||
username,
|
||||
profiles: [profile],
|
||||
// New users signing up via local auth will have the commenter role to
|
||||
// start with.
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
});
|
||||
const user = await insert(
|
||||
mongo,
|
||||
tenant,
|
||||
{
|
||||
email,
|
||||
username,
|
||||
profiles: [profile],
|
||||
// New users signing up via local auth will have the commenter role to
|
||||
// start with.
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
now
|
||||
);
|
||||
|
||||
// Send off to the passport handler.
|
||||
return handleSuccessfulLogin(user, signingConfig, req, res, next);
|
||||
|
||||
@@ -29,7 +29,8 @@ export const graphQLHandler = ({
|
||||
throw new Error("talk was not set");
|
||||
}
|
||||
|
||||
const { tenant, cache } = req.talk;
|
||||
// Pull out some useful properties from Talk.
|
||||
const { id, now, tenant, cache } = req.talk;
|
||||
|
||||
if (!cache) {
|
||||
throw new Error("cache was not set");
|
||||
@@ -43,6 +44,8 @@ export const graphQLHandler = ({
|
||||
schema,
|
||||
context: new TenantContext({
|
||||
...options,
|
||||
id,
|
||||
now,
|
||||
req,
|
||||
config,
|
||||
tenant,
|
||||
|
||||
@@ -93,14 +93,20 @@ export const installHandler = ({
|
||||
|
||||
// Install will throw if it can not create a Tenant, or it has already been
|
||||
// installed.
|
||||
const tenant = await install(mongo, redis, req.talk.cache.tenant, {
|
||||
...tenantInput,
|
||||
// Infer the Tenant domain via the hostname parameter.
|
||||
domain: req.hostname,
|
||||
// Add the locale that we had to default to the default locale from the
|
||||
// config.
|
||||
locale,
|
||||
});
|
||||
const tenant = await install(
|
||||
mongo,
|
||||
redis,
|
||||
req.talk.cache.tenant,
|
||||
{
|
||||
...tenantInput,
|
||||
// Infer the Tenant domain via the hostname parameter.
|
||||
domain: req.hostname,
|
||||
// Add the locale that we had to default to the default locale from the
|
||||
// config.
|
||||
locale,
|
||||
},
|
||||
req.talk.now
|
||||
);
|
||||
|
||||
// Pull the user details out of the input for the user.
|
||||
const { email, username, password } = userInput;
|
||||
@@ -113,12 +119,17 @@ export const installHandler = ({
|
||||
};
|
||||
|
||||
// Create the first admin user.
|
||||
await insert(mongo, tenant, {
|
||||
email,
|
||||
username,
|
||||
profiles: [profile],
|
||||
role: GQLUSER_ROLE.ADMIN,
|
||||
});
|
||||
await insert(
|
||||
mongo,
|
||||
tenant,
|
||||
{
|
||||
email,
|
||||
username,
|
||||
profiles: [profile],
|
||||
role: GQLUSER_ROLE.ADMIN,
|
||||
},
|
||||
req.talk.now
|
||||
);
|
||||
|
||||
// Send back the Tenant.
|
||||
return res.sendStatus(204);
|
||||
|
||||
@@ -75,6 +75,9 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
|
||||
throw new Error("logout requires a token on the request, none was found");
|
||||
}
|
||||
|
||||
// Talk is guarenteed at this point.
|
||||
const { now } = req.talk!;
|
||||
|
||||
// Decode the token.
|
||||
const decoded = jwt.decode(token, {});
|
||||
if (!decoded) {
|
||||
@@ -88,7 +91,7 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
|
||||
const { jti, exp }: LogoutToken = validate(LogoutTokenSchema, decoded);
|
||||
|
||||
// Compute the number of seconds that the token will be valid for.
|
||||
const validFor = exp - Date.now() / 1000;
|
||||
const validFor = exp - now.valueOf() / 1000;
|
||||
if (validFor > 0) {
|
||||
// Invalidate the token, the expiry is in the future and it needs to be
|
||||
// revoked.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user