mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
moderation panel cleanup
This commit is contained in:
1 parent
78ecef35a6
commit
b7fa2823f6
6 files changed
+625
-454
No files matched your search
+67
-52
@@ -1,24 +1,35 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import { Category, GetCategory, GetCategoryIsError, GetCategoryIsOk } from '@storyteller/components/src/api/category/GetCategory';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SetCategoryDeletionState, SetCategoryDeletionStateIsError, SetCategoryDeletionStateIsSuccess } from '@storyteller/components/src/api/moderation/category/SetCategoryDeletionState';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import {
|
||||
Category,
|
||||
GetCategory,
|
||||
GetCategoryIsError,
|
||||
GetCategoryIsOk,
|
||||
} from "@storyteller/components/src/api/category/GetCategory";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
SetCategoryDeletionState,
|
||||
SetCategoryDeletionStateIsError,
|
||||
SetCategoryDeletionStateIsSuccess,
|
||||
} from "@storyteller/components/src/api/moderation/category/SetCategoryDeletionState";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationCategoryDeletePage(props: Props) {
|
||||
const { token } : { token : string } = useParams();
|
||||
const { token }: { token: string } = useParams();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [category, setCategory] = useState<Category|undefined>(undefined);
|
||||
const [category, setCategory] = useState<Category | undefined>(undefined);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string|undefined>(undefined);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const getCategory = useCallback(async (categoryToken: string) => {
|
||||
const categoryList = await GetCategory(categoryToken);
|
||||
@@ -26,11 +37,11 @@ function ModerationCategoryDeletePage(props: Props) {
|
||||
if (GetCategoryIsOk(categoryList)) {
|
||||
const category = categoryList.category;
|
||||
setCategory(category);
|
||||
} else if (GetCategoryIsError(categoryList)) {
|
||||
} else if (GetCategoryIsError(categoryList)) {
|
||||
setErrorMessage("error fetching category");
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getCategory(token);
|
||||
}, [token, getCategory]);
|
||||
@@ -40,31 +51,37 @@ function ModerationCategoryDeletePage(props: Props) {
|
||||
}
|
||||
|
||||
if (category === undefined) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const currentlyDeleted = !!category?.deleted_at;
|
||||
|
||||
const deletePageTitle = currentlyDeleted ? "Undelete Category?" : "Delete Category?";
|
||||
const deleteButtonTitle = currentlyDeleted ? "Confirm Undelete Category" : "Confirm Delete Category";
|
||||
const deletePageTitle = currentlyDeleted
|
||||
? "Undelete Category?"
|
||||
: "Delete Category?";
|
||||
const deleteButtonTitle = currentlyDeleted
|
||||
? "Confirm Undelete Category"
|
||||
: "Confirm Delete Category";
|
||||
|
||||
const deleteButtonCss = currentlyDeleted ?
|
||||
"button is-warning is-large is-fullwidth" :
|
||||
"button is-danger is-large is-fullwidth";
|
||||
const deleteButtonCss = currentlyDeleted
|
||||
? "btn btn-destructive w-100"
|
||||
: "btn btn-destructive w-100";
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
const handleFormSubmit = async (
|
||||
ev: React.FormEvent<HTMLFormElement>
|
||||
): Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
let response = await SetCategoryDeletionState(token, !currentlyDeleted);
|
||||
|
||||
if (SetCategoryDeletionStateIsError(response)) {
|
||||
setErrorMessage('there was an error with the request'); // TODO: Fix error serialization
|
||||
setErrorMessage("there was an error with the request"); // TODO: Fix error serialization
|
||||
} else if (SetCategoryDeletionStateIsSuccess(response)) {
|
||||
history.push(WebUrl.moderationTtsCategoryEdit(token));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let errorFlash = <></>;
|
||||
|
||||
@@ -73,19 +90,20 @@ function ModerationCategoryDeletePage(props: Props) {
|
||||
<>
|
||||
<br />
|
||||
<article className="message is-error">
|
||||
<div className="message-body">
|
||||
{errorMessage}
|
||||
</div>
|
||||
<div className="message-body">{errorMessage}</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> {deletePageTitle} </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold">{deletePageTitle}</h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationTtsCategoryEdit(token)} text="Back to category edit page" />
|
||||
<BackLink
|
||||
link={WebUrl.moderationTtsCategoryEdit(token)}
|
||||
text="Back to category edit page"
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -96,57 +114,54 @@ function ModerationCategoryDeletePage(props: Props) {
|
||||
<table className="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><abbr title="Detail">Detail</abbr></th>
|
||||
<th><abbr title="Value">Value</abbr></th>
|
||||
<th>
|
||||
<abbr title="Detail">Detail</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Value">Value</abbr>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Category Name</th>
|
||||
<td>
|
||||
{category.name}
|
||||
</td>
|
||||
<td>{category.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creator</th>
|
||||
<td>
|
||||
<Link
|
||||
<Link
|
||||
to={WebUrl.userProfilePage(category?.creator_username || "")}
|
||||
>{category?.creator_username}</Link>
|
||||
>
|
||||
{category?.creator_username}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created On</th>
|
||||
<td>
|
||||
{category.created_at}
|
||||
</td>
|
||||
<td>{category.created_at}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Is Mod Approved?</th>
|
||||
<td>
|
||||
{category.is_mod_approved ? "Yes" : "No"}
|
||||
</td>
|
||||
<td>{category.is_mod_approved ? "Yes" : "No"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Is Currently Deleted?</th>
|
||||
<td>
|
||||
{currentlyDeleted ? "Yes" : "No"}
|
||||
</td>
|
||||
<td>{currentlyDeleted ? "Yes" : "No"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<form onSubmit={handleFormSubmit} className="my-4">
|
||||
<button className={deleteButtonCss}> {deleteButtonTitle} </button>
|
||||
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<BackLink link={WebUrl.moderationTtsCategoryEdit(token)} text="Back to category edit page" />
|
||||
<BackLink
|
||||
link={WebUrl.moderationTtsCategoryEdit(token)}
|
||||
text="Back to category edit page"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ModerationCategoryDeletePage };
|
||||
+270
-183
@@ -1,39 +1,62 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import { Category, GetCategory, GetCategoryIsError, GetCategoryIsOk } from '@storyteller/components/src/api/category/GetCategory';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EditCategory, EditCategoryIsError, EditCategoryIsSuccess, EditCategoryRequest } from '@storyteller/components/src/api/moderation/category/EditCategory';
|
||||
import { ListTtsCategories, ListTtsCategoriesIsError, ListTtsCategoriesIsOk, TtsCategory } from '@storyteller/components/src/api/category/ListTtsCategories';
|
||||
import { CategoryBreadcrumb } from '../../../_common/CategoryBreadcrumb';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import {
|
||||
Category,
|
||||
GetCategory,
|
||||
GetCategoryIsError,
|
||||
GetCategoryIsOk,
|
||||
} from "@storyteller/components/src/api/category/GetCategory";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
EditCategory,
|
||||
EditCategoryIsError,
|
||||
EditCategoryIsSuccess,
|
||||
EditCategoryRequest,
|
||||
} from "@storyteller/components/src/api/moderation/category/EditCategory";
|
||||
import {
|
||||
ListTtsCategories,
|
||||
ListTtsCategoriesIsError,
|
||||
ListTtsCategoriesIsOk,
|
||||
TtsCategory,
|
||||
} from "@storyteller/components/src/api/category/ListTtsCategories";
|
||||
import { CategoryBreadcrumb } from "../../../_common/CategoryBreadcrumb";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationTtsCategoryEditPage(props: Props) {
|
||||
const { token } : { token : string } = useParams();
|
||||
const { token }: { token: string } = useParams();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [category, setCategory] = useState<Category|undefined>(undefined);
|
||||
const [category, setCategory] = useState<Category | undefined>(undefined);
|
||||
|
||||
// Fields
|
||||
const [name, setName] = useState('');
|
||||
const [maybeSuperCategoryToken, setMaybeSuperCategoryToken] = useState<string|undefined>(undefined); // Optional
|
||||
const [maybeDropdownName, setMaybeDropdownName] = useState<string|undefined>(undefined); // Optional
|
||||
const [name, setName] = useState("");
|
||||
const [maybeSuperCategoryToken, setMaybeSuperCategoryToken] = useState<
|
||||
string | undefined
|
||||
>(undefined); // Optional
|
||||
const [maybeDropdownName, setMaybeDropdownName] = useState<
|
||||
string | undefined
|
||||
>(undefined); // Optional
|
||||
const [canDirectlyHaveModels, setCanDirectlyHaveModels] = useState(false);
|
||||
const [canHaveSubcategories, setCanHaveSubcategories] = useState(false);
|
||||
const [canOnlyModsApply, setCanOnlyModsApply] = useState(false);
|
||||
const [isModApproved, setIsModApproved] = useState(false);
|
||||
const [maybeModComments, setMaybeModComments] = useState<string|undefined>(undefined); // Optional
|
||||
const [maybeModComments, setMaybeModComments] = useState<string | undefined>(
|
||||
undefined
|
||||
); // Optional
|
||||
|
||||
// Additional object lookups to support parent categories
|
||||
const [allTtsCategories, setAllTtsCategories] = useState<TtsCategory[]>([]);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string|undefined>(undefined);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const getCategory = useCallback(async (categoryToken: string) => {
|
||||
const categoryList = await GetCategory(categoryToken);
|
||||
@@ -49,17 +72,17 @@ function ModerationTtsCategoryEditPage(props: Props) {
|
||||
setCanOnlyModsApply(category.can_only_mods_apply);
|
||||
setIsModApproved(category.is_mod_approved || false); // Default to false
|
||||
setMaybeModComments(category.maybe_mod_comments);
|
||||
} else if (GetCategoryIsError(categoryList)) {
|
||||
} else if (GetCategoryIsError(categoryList)) {
|
||||
setErrorMessage("error fetching category");
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const listTtsCategories = useCallback(async () => {
|
||||
const categoryList = await ListTtsCategories();
|
||||
|
||||
if (ListTtsCategoriesIsOk(categoryList)) {
|
||||
setAllTtsCategories(categoryList.categories);
|
||||
} else if (ListTtsCategoriesIsError(categoryList)) {
|
||||
} else if (ListTtsCategoriesIsError(categoryList)) {
|
||||
setErrorMessage("error listing all categories");
|
||||
}
|
||||
}, []);
|
||||
@@ -74,52 +97,68 @@ function ModerationTtsCategoryEditPage(props: Props) {
|
||||
}
|
||||
|
||||
if (category === undefined) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const handleNameChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
setName(value);
|
||||
}
|
||||
|
||||
const handleMaybeDropdownNameChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
let value = (ev.target as HTMLInputElement).value;
|
||||
setMaybeDropdownName( !!value ? value : undefined);
|
||||
}
|
||||
|
||||
const handleCanDirectlyHaveModelsChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanDirectlyHaveModels(value);
|
||||
}
|
||||
|
||||
const handleCanHaveSubcategoriesChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanHaveSubcategories(value);
|
||||
}
|
||||
|
||||
const handleCanOnlyModsApplyChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanOnlyModsApply(value);
|
||||
}
|
||||
|
||||
const handleIsModApprovedChange = (ev: React.FormEvent<HTMLSelectElement>) => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const updatedValue = value === "true" ? true : false;
|
||||
setIsModApproved(updatedValue)
|
||||
};
|
||||
|
||||
const handleSetSuperCategory = async (ev: React.FormEvent<HTMLSelectElement>) => {
|
||||
const superCategoryToken = (ev.target as HTMLSelectElement).value;
|
||||
let fixedSuperCategoryToken = !!superCategoryToken ? superCategoryToken : undefined;
|
||||
setMaybeSuperCategoryToken(fixedSuperCategoryToken);
|
||||
}
|
||||
const handleMaybeDropdownNameChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
let value = (ev.target as HTMLInputElement).value;
|
||||
setMaybeDropdownName(!!value ? value : undefined);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
const handleCanDirectlyHaveModelsChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanDirectlyHaveModels(value);
|
||||
};
|
||||
|
||||
const handleCanHaveSubcategoriesChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanHaveSubcategories(value);
|
||||
};
|
||||
|
||||
const handleCanOnlyModsApplyChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setCanOnlyModsApply(value);
|
||||
};
|
||||
|
||||
const handleIsModApprovedChange = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
) => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const updatedValue = value === "true" ? true : false;
|
||||
setIsModApproved(updatedValue);
|
||||
};
|
||||
|
||||
const handleSetSuperCategory = async (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
) => {
|
||||
const superCategoryToken = (ev.target as HTMLSelectElement).value;
|
||||
let fixedSuperCategoryToken = !!superCategoryToken
|
||||
? superCategoryToken
|
||||
: undefined;
|
||||
setMaybeSuperCategoryToken(fixedSuperCategoryToken);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (
|
||||
ev: React.FormEvent<HTMLFormElement>
|
||||
): Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
setErrorMessage(undefined);
|
||||
|
||||
let request : EditCategoryRequest = {
|
||||
let request: EditCategoryRequest = {
|
||||
name: name,
|
||||
maybe_dropdown_name: maybeDropdownName,
|
||||
maybe_mod_comments: maybeModComments,
|
||||
@@ -128,18 +167,18 @@ function ModerationTtsCategoryEditPage(props: Props) {
|
||||
can_have_subcategories: canHaveSubcategories,
|
||||
can_only_mods_apply: canOnlyModsApply,
|
||||
is_mod_approved: isModApproved,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await EditCategory(token, request);
|
||||
|
||||
if (EditCategoryIsError(response)) {
|
||||
setErrorMessage('there was an error with the request'); // TODO: Fix error serialization
|
||||
setErrorMessage("there was an error with the request"); // TODO: Fix error serialization
|
||||
} else if (EditCategoryIsSuccess(response)) {
|
||||
history.go(0); // NB: Force reload
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let errorFlash = <></>;
|
||||
|
||||
@@ -148,38 +187,42 @@ function ModerationTtsCategoryEditPage(props: Props) {
|
||||
<>
|
||||
<br />
|
||||
<article className="message is-error">
|
||||
<div className="message-body">
|
||||
{errorMessage}
|
||||
</div>
|
||||
<div className="message-body">{errorMessage}</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isModApprovedHtmlFormState = isModApproved ? "true" : "false";
|
||||
const maybeSuperCategoryTokenFormHtmlState = maybeSuperCategoryToken ? maybeSuperCategoryToken : "";
|
||||
const maybeSuperCategoryTokenFormHtmlState = maybeSuperCategoryToken
|
||||
? maybeSuperCategoryToken
|
||||
: "";
|
||||
|
||||
const superCategoryOptions = allTtsCategories.filter(category => {
|
||||
const isSelf = token === category.category_token;
|
||||
const cannotAdd = !category.can_have_subcategories;
|
||||
return !isSelf && !cannotAdd;
|
||||
}).map(category => {
|
||||
return (
|
||||
<>
|
||||
<option
|
||||
value={category.category_token}
|
||||
key={category.category_token}>{category.name}</option>
|
||||
</>
|
||||
)
|
||||
});
|
||||
const superCategoryOptions = allTtsCategories
|
||||
.filter((category) => {
|
||||
const isSelf = token === category.category_token;
|
||||
const cannotAdd = !category.can_have_subcategories;
|
||||
return !isSelf && !cannotAdd;
|
||||
})
|
||||
.map((category) => {
|
||||
return (
|
||||
<>
|
||||
<option value={category.category_token} key={category.category_token}>
|
||||
{category.name}
|
||||
</option>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const currentlyDeleted = !!category?.deleted_at;
|
||||
|
||||
const deleteButtonTitle = currentlyDeleted ? "Undelete Category?" : "Delete Category?";
|
||||
const deleteButtonTitle = currentlyDeleted
|
||||
? "Undelete Category?"
|
||||
: "Delete Category?";
|
||||
|
||||
const deleteButtonCss = currentlyDeleted ?
|
||||
"button is-warning is-large is-fullwidth" :
|
||||
"button is-danger is-large is-fullwidth";
|
||||
const deleteButtonCss = currentlyDeleted
|
||||
? "btn btn-destructive w-100"
|
||||
: "btn btn-destructive w-100";
|
||||
|
||||
let deletedNotice = <></>;
|
||||
|
||||
@@ -199,140 +242,184 @@ function ModerationTtsCategoryEditPage(props: Props) {
|
||||
const categoryHierarchy = recursiveBuildHierarchy(allTtsCategories, token);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> Moderate TTS Category </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold"> Moderate TTS Category </h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationTtsCategoryList()} text="Back to category list" />
|
||||
<BackLink
|
||||
link={WebUrl.moderationTtsCategoryList()}
|
||||
text="Back to category list"
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<CategoryBreadcrumb categoryHierarchy={categoryHierarchy} isCategoryMod={true} leafHasModels={false} />
|
||||
<br />
|
||||
<CategoryBreadcrumb
|
||||
categoryHierarchy={categoryHierarchy}
|
||||
isCategoryMod={true}
|
||||
leafHasModels={false}
|
||||
/>
|
||||
|
||||
{errorFlash}
|
||||
{deletedNotice}
|
||||
|
||||
<br />
|
||||
<form
|
||||
onSubmit={handleFormSubmit}
|
||||
className="d-flex flex-column gap-4 mt-5"
|
||||
>
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Mod Approval (sets public list visibility)
|
||||
</label>
|
||||
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<label className="label">Mod Approval (sets public list visibility)</label>
|
||||
|
||||
<div className="select is-info is-large">
|
||||
<select name="approve" value={isModApprovedHtmlFormState} onChange={handleIsModApprovedChange}>
|
||||
<option value="true">Approve</option>
|
||||
<option value="false">Disapprove</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Category Name</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="Category Name" value={name} onChange={handleNameChange} />
|
||||
<div className="select is-info is-large">
|
||||
<select
|
||||
name="approve"
|
||||
value={isModApprovedHtmlFormState}
|
||||
onChange={handleIsModApprovedChange}
|
||||
className="form-select rounded"
|
||||
>
|
||||
<option value="true">Approve</option>
|
||||
<option value="false">Disapprove</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Dropdown Name Override (optional)</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Dropdown Name"
|
||||
value={maybeDropdownName || ""}
|
||||
onChange={handleMaybeDropdownNameChange} />
|
||||
</div>
|
||||
<p>(eg. if the category name is "Gender", this might be named "By Gender" for the dropdown.)</p>
|
||||
<div>
|
||||
<label className="sub-title">Category Name</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Category Name"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<label className="label">Permission Flags</label>
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canOnlyModsApply}
|
||||
onChange={handleCanOnlyModsApplyChange} />
|
||||
Can only mods apply this category? (Model authors can't add this themselves. For "Best of" and other special categories.)
|
||||
</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label className="label">Topology (Children)</label>
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canDirectlyHaveModels}
|
||||
onChange={handleCanDirectlyHaveModelsChange} />
|
||||
Can this category be directly assigned to models? (If not, it's only a super category.)
|
||||
</label>
|
||||
|
||||
<br />
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canHaveSubcategories}
|
||||
onChange={handleCanHaveSubcategoriesChange} />
|
||||
Can this category have subcategories?
|
||||
</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label className="label">Topology (Optional Parent Category)</label>
|
||||
|
||||
<div className="field">
|
||||
<div>
|
||||
<label className="sub-title">Dropdown Name Override (optional)</label>
|
||||
<div className="control">
|
||||
<div className="select is-info">
|
||||
<select onChange={handleSetSuperCategory} value={maybeSuperCategoryTokenFormHtmlState}>
|
||||
<option value="" key="">None (this is optional)</option>
|
||||
{superCategoryOptions}
|
||||
</select>
|
||||
</div>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Dropdown Name"
|
||||
value={maybeDropdownName || ""}
|
||||
onChange={handleMaybeDropdownNameChange}
|
||||
/>
|
||||
</div>
|
||||
<p className="form-text">
|
||||
(eg. if the category name is "Gender", this might be named "By
|
||||
Gender" for the dropdown.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column py-3">
|
||||
<label className="sub-title">Permission Flags</label>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canOnlyModsApply}
|
||||
onChange={handleCanOnlyModsApplyChange}
|
||||
className="form-check-input"
|
||||
id="checkCanOnlyModsApplyChange"
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="checkCanOnlyModsApplyChange"
|
||||
>
|
||||
Can only mods apply this category? (Model authors can't add this
|
||||
themselves. For "Best of" and other special categories.)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="sub-title">Topology (Children)</label>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canDirectlyHaveModels}
|
||||
onChange={handleCanDirectlyHaveModelsChange}
|
||||
className="form-check-input"
|
||||
id="checkCanDirectlyHaveModelsChange"
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="checkCanDirectlyHaveModelsChange"
|
||||
>
|
||||
Can this category be directly assigned to models? (If not, it's
|
||||
only a super category.)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={canHaveSubcategories}
|
||||
onChange={handleCanHaveSubcategoriesChange}
|
||||
id="checkCanHaveSubcategoriesChange"
|
||||
className="form-check-input"
|
||||
/>
|
||||
<label htmlFor="checkCanHaveSubcategoriesChange">
|
||||
Can this category have subcategories?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Topology (Optional Parent Category)
|
||||
</label>
|
||||
|
||||
<button className="button is-link is-large is-fullwidth"> Save Changes </button>
|
||||
<div className="form-group">
|
||||
<select
|
||||
onChange={handleSetSuperCategory}
|
||||
value={maybeSuperCategoryTokenFormHtmlState}
|
||||
className="form-select rounded"
|
||||
>
|
||||
<option value="" key="">
|
||||
None (this is optional)
|
||||
</option>
|
||||
{superCategoryOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-3 my-4">
|
||||
<button className="btn btn-primary w-100">Save Changes</button>
|
||||
<Link
|
||||
className={deleteButtonCss}
|
||||
to={WebUrl.moderationCategoryDeletePage(token)}
|
||||
>
|
||||
{deleteButtonTitle}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
className={deleteButtonCss}
|
||||
to={WebUrl.moderationCategoryDeletePage(token)}
|
||||
>{deleteButtonTitle}</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<BackLink link={WebUrl.moderationTtsCategoryList()} text="Back to category list" />
|
||||
<div className="d-flex flex-column gap-2">
|
||||
<BackLink
|
||||
link={WebUrl.moderationTtsCategoryList()}
|
||||
text="Back to category list"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// FIXME: This has been implemented three times, slightly differently
|
||||
function recursiveBuildHierarchy(allCategories: TtsCategory[], currentToken: string): TtsCategory[] {
|
||||
let found = allCategories.find(category => category.category_token === currentToken);
|
||||
function recursiveBuildHierarchy(
|
||||
allCategories: TtsCategory[],
|
||||
currentToken: string
|
||||
): TtsCategory[] {
|
||||
let found = allCategories.find(
|
||||
(category) => category.category_token === currentToken
|
||||
);
|
||||
if (found === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (found.maybe_super_category_token === undefined) {
|
||||
return [found];
|
||||
}
|
||||
return [...recursiveBuildHierarchy(allCategories, found.maybe_super_category_token), found];
|
||||
return [
|
||||
...recursiveBuildHierarchy(allCategories, found.maybe_super_category_token),
|
||||
found,
|
||||
];
|
||||
}
|
||||
|
||||
export { ModerationTtsCategoryEditPage };
|
||||
+189
-132
@@ -1,37 +1,53 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { Gravatar } from '@storyteller/components/src/elements/Gravatar';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import { ListTtsCategoriesForModeration, ListTtsCategoriesForModerationIsError, ListTtsCategoriesForModerationIsOk, ListTtsCategoriesTriState, ModerationTtsCategory } from '@storyteller/components/src/api/moderation/category/ListTtsCategoriesForModeration';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBox, faBoxOpen, faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CategoryBreadcrumb } from '../../../_common/CategoryBreadcrumb';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
// import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import {
|
||||
ListTtsCategoriesForModeration,
|
||||
ListTtsCategoriesForModerationIsError,
|
||||
ListTtsCategoriesForModerationIsOk,
|
||||
ListTtsCategoriesTriState,
|
||||
ModerationTtsCategory,
|
||||
} from "@storyteller/components/src/api/moderation/category/ListTtsCategoriesForModeration";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBox, faBoxOpen, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { CategoryBreadcrumb } from "../../../_common/CategoryBreadcrumb";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
interface SortableCategoryHierarchy {
|
||||
hierarchy: ModerationTtsCategory[],
|
||||
hierarchy: ModerationTtsCategory[];
|
||||
// NB: While we could pull out the last item of the hierarchy list, this is more convenient.
|
||||
category: ModerationTtsCategory,
|
||||
category: ModerationTtsCategory;
|
||||
}
|
||||
|
||||
function ModerationTtsCategoryListPage(props: Props) {
|
||||
const [ttsCategories, setTtsCategories] = useState<ModerationTtsCategory[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string|undefined>(undefined);
|
||||
const [ttsCategories, setTtsCategories] = useState<ModerationTtsCategory[]>(
|
||||
[]
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [deletedView, setDeletedView] = useState<ListTtsCategoriesTriState>(ListTtsCategoriesTriState.Exclude);
|
||||
const [unapprovedView, setUnapprovedView] = useState<ListTtsCategoriesTriState>(ListTtsCategoriesTriState.Include);
|
||||
const [deletedView, setDeletedView] = useState<ListTtsCategoriesTriState>(
|
||||
ListTtsCategoriesTriState.Exclude
|
||||
);
|
||||
const [unapprovedView, setUnapprovedView] =
|
||||
useState<ListTtsCategoriesTriState>(ListTtsCategoriesTriState.Include);
|
||||
|
||||
const listTtsCategories = useCallback(async () => {
|
||||
const categoryList = await ListTtsCategoriesForModeration(deletedView, unapprovedView);
|
||||
const categoryList = await ListTtsCategoriesForModeration(
|
||||
deletedView,
|
||||
unapprovedView
|
||||
);
|
||||
|
||||
if (ListTtsCategoriesForModerationIsOk(categoryList)) {
|
||||
setTtsCategories(categoryList.categories);
|
||||
} else if (ListTtsCategoriesForModerationIsError(categoryList)) {
|
||||
} else if (ListTtsCategoriesForModerationIsError(categoryList)) {
|
||||
setErrorMessage("error listing all categories");
|
||||
}
|
||||
}, [deletedView, unapprovedView]);
|
||||
@@ -50,7 +66,7 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
if (maybeTriState !== undefined) {
|
||||
setDeletedView(maybeTriState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnapprovedChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
@@ -58,7 +74,7 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
if (maybeTriState !== undefined) {
|
||||
setUnapprovedView(maybeTriState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let errorFlash = <></>;
|
||||
|
||||
@@ -66,80 +82,89 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
errorFlash = (
|
||||
<>
|
||||
<article className="message is-error">
|
||||
<div className="message-body">
|
||||
{errorMessage}
|
||||
</div>
|
||||
<div className="message-body">{errorMessage}</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let sortableHierarchies : SortableCategoryHierarchy[] = ttsCategories.map(category => {
|
||||
const categoryHierarchy = recursiveBuildHierarchy(ttsCategories, category.category_token);
|
||||
return {
|
||||
hierarchy: categoryHierarchy,
|
||||
category: category,
|
||||
}
|
||||
}).sort((first, second) => {
|
||||
let maxLength = Math.max(first.hierarchy.length, second.hierarchy.length);
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
let f = first.hierarchy[i];
|
||||
let s = second.hierarchy[i];
|
||||
if (f !== undefined && s !== undefined) {
|
||||
let compared = f.name.localeCompare(s.name)
|
||||
if (compared !== 0) {
|
||||
return compared;
|
||||
let sortableHierarchies: SortableCategoryHierarchy[] = ttsCategories
|
||||
.map((category) => {
|
||||
const categoryHierarchy = recursiveBuildHierarchy(
|
||||
ttsCategories,
|
||||
category.category_token
|
||||
);
|
||||
return {
|
||||
hierarchy: categoryHierarchy,
|
||||
category: category,
|
||||
};
|
||||
})
|
||||
.sort((first, second) => {
|
||||
let maxLength = Math.max(first.hierarchy.length, second.hierarchy.length);
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
let f = first.hierarchy[i];
|
||||
let s = second.hierarchy[i];
|
||||
if (f !== undefined && s !== undefined) {
|
||||
let compared = f.name.localeCompare(s.name);
|
||||
if (compared !== 0) {
|
||||
return compared;
|
||||
}
|
||||
} else if (f === undefined) {
|
||||
return -1;
|
||||
} else if (s === undefined) {
|
||||
return 1;
|
||||
}
|
||||
} else if (f === undefined) {
|
||||
return -1;
|
||||
} else if (s === undefined) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
return -1;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> Moderate TTS Categories </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold"> Moderate TTS Categories </h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
|
||||
<br />
|
||||
<Link
|
||||
to={WebUrl.createCategoryPage()}
|
||||
className="btn btn-primary w-100 my-4"
|
||||
>
|
||||
Create new category
|
||||
</Link>
|
||||
|
||||
{errorFlash}
|
||||
|
||||
<br />
|
||||
|
||||
<div className="control">
|
||||
<strong>Show Unapproved:</strong>
|
||||
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="include"
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="include"
|
||||
checked={unapprovedView === ListTtsCategoriesTriState.Include}
|
||||
onChange={handleUnapprovedChange} />
|
||||
onChange={handleUnapprovedChange}
|
||||
/>
|
||||
Include
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="exclude"
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="exclude"
|
||||
checked={unapprovedView === ListTtsCategoriesTriState.Exclude}
|
||||
onChange={handleUnapprovedChange} />
|
||||
onChange={handleUnapprovedChange}
|
||||
/>
|
||||
Exclude
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="only"
|
||||
<input
|
||||
type="radio"
|
||||
name="unapproved"
|
||||
value="only"
|
||||
checked={unapprovedView === ListTtsCategoriesTriState.Only}
|
||||
onChange={handleUnapprovedChange} />
|
||||
onChange={handleUnapprovedChange}
|
||||
/>
|
||||
Only
|
||||
</label>
|
||||
</div>
|
||||
@@ -148,36 +173,39 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
<strong>Show Deleted:</strong>
|
||||
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="include"
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="include"
|
||||
checked={deletedView === ListTtsCategoriesTriState.Include}
|
||||
onChange={handleDeletedChange} />
|
||||
onChange={handleDeletedChange}
|
||||
/>
|
||||
Include
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="exclude"
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="exclude"
|
||||
checked={deletedView === ListTtsCategoriesTriState.Exclude}
|
||||
onChange={handleDeletedChange} />
|
||||
onChange={handleDeletedChange}
|
||||
/>
|
||||
Exclude
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="only"
|
||||
<input
|
||||
type="radio"
|
||||
name="deleted"
|
||||
value="only"
|
||||
checked={deletedView === ListTtsCategoriesTriState.Only}
|
||||
onChange={handleDeletedChange} />
|
||||
onChange={handleDeletedChange}
|
||||
/>
|
||||
Only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<table className="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -189,70 +217,98 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortableHierarchies.map(sortableHierarchy => {
|
||||
{sortableHierarchies.map((sortableHierarchy) => {
|
||||
const category = sortableHierarchy.category;
|
||||
const categoryHierarchy = sortableHierarchy.hierarchy;
|
||||
|
||||
let modOnlyIcon = category.can_only_mods_apply ?
|
||||
<> <FontAwesomeIcon icon={faLock} title={"mod only"} /></> :
|
||||
<></>;
|
||||
let modOnlyIcon = category.can_only_mods_apply ? (
|
||||
<>
|
||||
|
||||
<FontAwesomeIcon icon={faLock} title={"mod only"} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
let parentCategoryIcon = <></>;
|
||||
|
||||
|
||||
if (category.can_have_subcategories) {
|
||||
if (category.can_directly_have_models) {
|
||||
parentCategoryIcon = <><FontAwesomeIcon icon={faBoxOpen} title={"can have subcategories and models"} /> </>
|
||||
parentCategoryIcon = (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon={faBoxOpen}
|
||||
title={"can have subcategories and models"}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
parentCategoryIcon = <><FontAwesomeIcon icon={faBox} title={"can have subcategories"} /> </>
|
||||
parentCategoryIcon = (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon={faBox}
|
||||
title={"can have subcategories"}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let creatorLink = <span />;
|
||||
|
||||
if (!!category?.creator_display_name) {
|
||||
const creatorUrl = WebUrl.userProfilePage(category?.creator_username || "username error");
|
||||
const creatorUrl = WebUrl.userProfilePage(
|
||||
category?.creator_username || "username error"
|
||||
);
|
||||
creatorLink = (
|
||||
<span className="white-space-nowrap">
|
||||
<Gravatar
|
||||
{/* <Gravatar
|
||||
size={15}
|
||||
username={category.creator_display_name || ""}
|
||||
email_hash={category.creator_gravatar_hash || ""}
|
||||
/>
|
||||
username={category.creator_display_name || ""}
|
||||
email_hash={category.creator_gravatar_hash || ""}
|
||||
/> */}
|
||||
|
||||
<Link to={creatorUrl}>{category.creator_display_name}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let approved = 'not set';
|
||||
let approved = "not set";
|
||||
if (category.is_mod_approved === undefined) {
|
||||
approved = 'not set';
|
||||
approved = "not set";
|
||||
} else if (category.is_mod_approved === true) {
|
||||
approved = 'approved';
|
||||
approved = "approved";
|
||||
} else if (category.is_mod_approved === false) {
|
||||
approved = 'DISAPPROVED';
|
||||
approved = "DISAPPROVED";
|
||||
}
|
||||
|
||||
let deleted = !!category.deleted_at ? 'DELETED' : 'No';
|
||||
let deleted = !!category.deleted_at ? "DELETED" : "No";
|
||||
|
||||
return (
|
||||
<tr key={category.category_token}>
|
||||
<td>
|
||||
{parentCategoryIcon}
|
||||
{modOnlyIcon}
|
||||
<CategoryBreadcrumb categoryHierarchy={categoryHierarchy} isCategoryMod={true} leafHasModels={false} disableLinks={true} />
|
||||
<CategoryBreadcrumb
|
||||
categoryHierarchy={categoryHierarchy}
|
||||
isCategoryMod={true}
|
||||
leafHasModels={false}
|
||||
disableLinks={true}
|
||||
/>
|
||||
</td>
|
||||
<td>{creatorLink}</td>
|
||||
<td>{approved}</td>
|
||||
<td>{deleted}</td>
|
||||
<td>
|
||||
{creatorLink}
|
||||
</td>
|
||||
<td>
|
||||
{approved}
|
||||
</td>
|
||||
<td>
|
||||
{deleted}
|
||||
</td>
|
||||
<td>
|
||||
<Link to={WebUrl.moderationTtsCategoryEdit(category.category_token)}>edit</Link>
|
||||
<Link
|
||||
to={WebUrl.moderationTtsCategoryEdit(
|
||||
category.category_token
|
||||
)}
|
||||
>
|
||||
edit
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -260,48 +316,49 @@ function ModerationTtsCategoryListPage(props: Props) {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<strong>Note: </strong> Only approved, non-deleted categories will show up publicly.
|
||||
New category suggestions by non-mods are "unapproved" by default.
|
||||
Use "deletion" to hide categories you don't want to deal with anymore.
|
||||
<p className="my-4">
|
||||
<strong>Note: </strong> Only approved, non-deleted categories will show
|
||||
up publicly. New category suggestions by non-mods are "unapproved" by
|
||||
default. Use "deletion" to hide categories you don't want to deal with
|
||||
anymore.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to={WebUrl.createCategoryPage()}
|
||||
className="button is-info is-large is-fullwidth"
|
||||
>Create new category</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function StringToTriState(state: string) : ListTtsCategoriesTriState | undefined {
|
||||
function StringToTriState(
|
||||
state: string
|
||||
): ListTtsCategoriesTriState | undefined {
|
||||
switch (state) {
|
||||
case 'include':
|
||||
case "include":
|
||||
return ListTtsCategoriesTriState.Include;
|
||||
case 'exclude':
|
||||
case "exclude":
|
||||
return ListTtsCategoriesTriState.Exclude;
|
||||
case 'only':
|
||||
case "only":
|
||||
return ListTtsCategoriesTriState.Only;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This has been implemented three times, slightly differently
|
||||
function recursiveBuildHierarchy(allCategories: ModerationTtsCategory[], currentToken: string): ModerationTtsCategory[] {
|
||||
let found = allCategories.find(category => category.category_token === currentToken);
|
||||
function recursiveBuildHierarchy(
|
||||
allCategories: ModerationTtsCategory[],
|
||||
currentToken: string
|
||||
): ModerationTtsCategory[] {
|
||||
let found = allCategories.find(
|
||||
(category) => category.category_token === currentToken
|
||||
);
|
||||
if (found === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (found.maybe_super_category_token === undefined) {
|
||||
return [found];
|
||||
}
|
||||
return [...recursiveBuildHierarchy(allCategories, found.maybe_super_category_token), found];
|
||||
return [
|
||||
...recursiveBuildHierarchy(allCategories, found.maybe_super_category_token),
|
||||
found,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
export { ModerationTtsCategoryListPage };
|
||||
+42
-34
@@ -1,19 +1,25 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { Gravatar } from '@storyteller/components/src/elements/Gravatar';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import { GetPendingW2lTemplates, GetPendingW2lTemplatesIsOk, PendingW2lTemplatesEntryForList } from '@storyteller/components/src/api/moderation/w2l/GetPendingW2lTemplates';
|
||||
import { PhotoVideoIcon } from '../../../_icons/PhotoVideoIcon';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import {
|
||||
GetPendingW2lTemplates,
|
||||
GetPendingW2lTemplatesIsOk,
|
||||
PendingW2lTemplatesEntryForList,
|
||||
} from "@storyteller/components/src/api/moderation/w2l/GetPendingW2lTemplates";
|
||||
import { PhotoVideoIcon } from "../../../_icons/PhotoVideoIcon";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
const [templates, setTemplates] = useState<Array<PendingW2lTemplatesEntryForList>>([]);
|
||||
const [templates, setTemplates] = useState<
|
||||
Array<PendingW2lTemplatesEntryForList>
|
||||
>([]);
|
||||
|
||||
const getUsers = useCallback(async () => {
|
||||
const response = await GetPendingW2lTemplates();
|
||||
@@ -21,16 +27,14 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
if (GetPendingW2lTemplatesIsOk(response)) {
|
||||
setTemplates(response.templates);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, [getUsers]);
|
||||
|
||||
|
||||
if (!templates) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (!props.sessionWrapper.canApproveW2lTemplates()) {
|
||||
@@ -38,12 +42,14 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let rows : Array<JSX.Element> = [];
|
||||
let rows: Array<JSX.Element> = [];
|
||||
|
||||
templates.forEach(template=> {
|
||||
templates.forEach((template) => {
|
||||
const createTime = new Date(template.created_at);
|
||||
const relativeCreateTime = formatDistance(createTime, now, { addSuffix: true });
|
||||
|
||||
const relativeCreateTime = formatDistance(createTime, now, {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
rows.push(
|
||||
<tr key={template.template_token}>
|
||||
<td>
|
||||
@@ -55,7 +61,11 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
</td>
|
||||
<td>
|
||||
<Link to={WebUrl.userProfilePage(template.creator_display_name)}>
|
||||
<Gravatar username={template.creator_display_name} email_hash={template.creator_gravatar_hash} size={12} />
|
||||
<Gravatar
|
||||
username={template.creator_display_name}
|
||||
email_hash={template.creator_gravatar_hash}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
{template.creator_display_name}
|
||||
</Link>
|
||||
@@ -63,12 +73,12 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
<td>{template.template_type}</td>
|
||||
<td>{relativeCreateTime}</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> Unapproved W2L Templates </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold"> Unapproved W2L Templates </h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
|
||||
@@ -82,22 +92,22 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Don't delete templates unless they overwhelmingly break the rules.
|
||||
Users can use templates in private without them necessarily being approved for public use.
|
||||
Don't delete templates unless they overwhelmingly break the rules. Users
|
||||
can use templates in private without them necessarily being approved for
|
||||
public use.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Setting approved = TRUE will show the template on the main site for everyone to use.
|
||||
Setting approved = FALSE will still let the author and people directly accessing via
|
||||
URL to use the template, but will not make it public.
|
||||
It will also remove it from the moderation queue.
|
||||
Setting approved = TRUE will show the template on the main site for
|
||||
everyone to use. Setting approved = FALSE will still let the author and
|
||||
people directly accessing via URL to use the template, but will not make
|
||||
it public. It will also remove it from the moderation queue.
|
||||
</p>
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<table className="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -107,12 +117,10 @@ function ModerationPendingW2lTemplatesFc(props: Props) {
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ModerationPendingW2lTemplatesFc };
|
||||
+37
-30
@@ -1,14 +1,18 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { Gravatar } from '@storyteller/components/src/elements/Gravatar';
|
||||
import { GetUserList, GetUserListIsOk, UserForList } from '@storyteller/components/src/api/moderation/user/GetUserList';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import {
|
||||
GetUserList,
|
||||
GetUserListIsOk,
|
||||
UserForList,
|
||||
} from "@storyteller/components/src/api/moderation/user/GetUserList";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationUserListFc(props: Props) {
|
||||
@@ -20,16 +24,14 @@ function ModerationUserListFc(props: Props) {
|
||||
if (GetUserListIsOk(response)) {
|
||||
setUserList(response.users);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, [getUsers]);
|
||||
|
||||
|
||||
if (!userList) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (!props.sessionWrapper.canBanUsers()) {
|
||||
@@ -37,21 +39,29 @@ function ModerationUserListFc(props: Props) {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let rows : Array<JSX.Element> = [];
|
||||
let rows: Array<JSX.Element> = [];
|
||||
|
||||
userList.forEach(user => {
|
||||
userList.forEach((user) => {
|
||||
const createTime = new Date(user.created_at);
|
||||
const relativeCreateTime = formatDistance(createTime, now, { addSuffix: true });
|
||||
|
||||
const relativeCreateTime = formatDistance(createTime, now, {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
const updateTime = new Date(user.updated_at);
|
||||
const relativeUpdateTime = formatDistance(updateTime, now, { addSuffix: true });
|
||||
const relativeUpdateTime = formatDistance(updateTime, now, {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
rows.push(
|
||||
<tr key={user.user_token}>
|
||||
<td>{user.user_id}</td>
|
||||
<td>
|
||||
<Link to={WebUrl.userProfilePage(user.display_name)}>
|
||||
<Gravatar username={user.display_name} email_hash={user.gravatar_hash} size={12} />
|
||||
<Gravatar
|
||||
username={user.display_name}
|
||||
email_hash={user.gravatar_hash}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
{user.display_name}
|
||||
</Link>
|
||||
@@ -61,12 +71,12 @@ function ModerationUserListFc(props: Props) {
|
||||
<td>{user.user_role_slug}</td>
|
||||
<td>{user.is_banned ? "banned" : ""}</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> User list </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold"> User list </h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
|
||||
@@ -74,14 +84,13 @@ function ModerationUserListFc(props: Props) {
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Brandon did the <strong>bare minimum</strong> to get this working.
|
||||
It isn't paginated and will break once we get a lot of users.
|
||||
It's also not sortable.
|
||||
I'll need to cycle back and fix it after we launch.
|
||||
Brandon did the <strong>bare minimum</strong> to get this working. It
|
||||
isn't paginated and will break once we get a lot of users. It's also not
|
||||
sortable. I'll need to cycle back and fix it after we launch.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -93,12 +102,10 @@ function ModerationUserListFc(props: Props) {
|
||||
<th>Is Banned?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ModerationUserListFc };
|
||||
+20
-23
@@ -1,11 +1,14 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { WebUrl } from '../../../../../common/WebUrl';
|
||||
import { BackLink } from '../../../_common/BackLink';
|
||||
import { GetVoiceInventoryStats, GetVoiceInventoryStatsIsOk } from '@storyteller/components/src/api/moderation/stats/GetVoiceInventoryStats';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import {
|
||||
GetVoiceInventoryStats,
|
||||
GetVoiceInventoryStatsIsOk,
|
||||
} from "@storyteller/components/src/api/moderation/stats/GetVoiceInventoryStats";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationVoiceStatsFc(props: Props) {
|
||||
@@ -19,7 +22,7 @@ function ModerationVoiceStatsFc(props: Props) {
|
||||
setPublicVoicesCount(response.public_voices_count);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const reloadStats = useCallback(async () => {
|
||||
getVoiceStats();
|
||||
}, [getVoiceStats]);
|
||||
@@ -28,9 +31,8 @@ function ModerationVoiceStatsFc(props: Props) {
|
||||
reloadStats();
|
||||
}, [reloadStats]);
|
||||
|
||||
|
||||
if (allVoicesCount === -1 && publicVoicesCount === -1) {
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
|
||||
if (!props.sessionWrapper.canEditOtherUsersTtsModels()) {
|
||||
@@ -38,38 +40,33 @@ function ModerationVoiceStatsFc(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> Voice Stats </h1>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold"> Voice Stats </h1>
|
||||
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
<table className="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Public voice vount
|
||||
</th>
|
||||
<th>Public voice vount</th>
|
||||
<td>{publicVoicesCount} voices </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
All voice count (incl banned)
|
||||
</th>
|
||||
<th>All voice count (incl banned)</th>
|
||||
<td>{allVoicesCount} voices </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
<button
|
||||
className="button is-info is-large is-fullwidth"
|
||||
onClick={() => reloadStats()}
|
||||
>Reload</button>
|
||||
<button className="btn btn-primary w-100" onClick={() => reloadStats()}>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ModerationVoiceStatsFc };
|
||||
Reference in new issue
Block a user