import { myAuthRequired, setIsoData } from "@utils/app";
+import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno";
import {
CreateCustomEmoji,
import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags";
-import { Icon } from "../common/icon";
+import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator";
interface EmojiFormProps {
onEdit(form: EditCustomEmoji): void;
onCreate(form: CreateCustomEmoji): void;
onDelete(form: DeleteCustomEmoji): void;
- loading: boolean;
}
interface EmojiFormState {
keywords: string;
changed: boolean;
page: number;
+ loading: boolean;
}
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
keywords: x.keywords.map(x => x.keyword).join(" "),
changed: false,
page: 1 + Math.floor(index / this.itemsPerPage),
+ loading: false,
})),
page: 1,
};
</div>
)}
<div className="table-responsive">
- <table id="emojis_table" className="table table-sm table-hover">
+ <table
+ id="emojis_table"
+ className="table table-sm table-hover align-middle"
+ >
<thead className="pointer">
<tr>
<th>{I18NextService.i18n.t("column_emoji")}</th>
.map((cv, index) => (
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
<td style="text-align:center;">
- <label
- htmlFor={index.toString()}
- className="pointer text-muted small font-weight-bold"
- >
- {cv.image_url.length > 0 && (
- <img
- alt={cv.alt_text}
- className="icon-emoji-admin"
- src={cv.image_url}
+ {cv.image_url.length > 0 && (
+ <img
+ className="icon-emoji-admin"
+ src={cv.image_url}
+ alt={cv.alt_text}
+ />
+ )}
+ {cv.image_url.length === 0 && (
+ <label
+ // TODO: Fix this linting violation
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+ tabIndex={0}
+ className="btn btn-sm btn-secondary pointer"
+ htmlFor={`file-uploader-${index}`}
+ data-tippy-content={I18NextService.i18n.t(
+ "upload_image"
+ )}
+ >
+ {capitalizeFirstLetter(
+ I18NextService.i18n.t("upload")
+ )}
+ <input
+ name={`file-uploader-${index}`}
+ id={`file-uploader-${index}`}
+ type="file"
+ accept="image/*"
+ className="d-none"
+ onChange={linkEvent(
+ { form: this, index: index },
+ this.handleImageUpload
+ )}
/>
- )}
- {cv.image_url.length == 0 && (
- <span className="btn btn-sm btn-secondary">
- Upload
- </span>
- )}
- </label>
- <input
- name={index.toString()}
- id={index.toString()}
- type="file"
- accept="image/*"
- className="d-none"
- onChange={linkEvent(
- { form: this, index: index },
- this.handleImageUpload
- )}
- />
+ </label>
+ )}
</td>
<td className="text-right">
<input
<span title={this.getEditTooltip(cv)}>
<button
className={
- (cv.changed ? "text-success " : "text-muted ") +
- "btn btn-link btn-animate"
+ (this.canEdit(cv)
+ ? "text-success "
+ : "text-muted ") + "btn btn-link btn-animate"
}
onClick={linkEvent(
{ i: this, cv: cv },
)}
data-tippy-content={I18NextService.i18n.t("save")}
aria-label={I18NextService.i18n.t("save")}
- disabled={
- this.props.loading ||
- !this.canEdit(cv) ||
- !cv.changed
- }
+ disabled={!this.canEdit(cv)}
>
- {/* <Icon
- icon="edit"
- classes={`icon-inline`}
- /> */}
- Save
+ {cv.loading ? (
+ <Spinner />
+ ) : (
+ capitalizeFirstLetter(
+ I18NextService.i18n.t("save")
+ )
+ )}
</button>
</span>
<button
)}
data-tippy-content={I18NextService.i18n.t("delete")}
aria-label={I18NextService.i18n.t("delete")}
- disabled={this.props.loading}
+ disabled={cv.loading}
title={I18NextService.i18n.t("delete")}
>
<Icon
icon="trash"
- classes={`icon-inline text-danger`}
+ classes="icon-inline text-danger"
/>
</button>
</div>
this.state.customEmojis.filter(
x => x.shortcode == cv.shortcode && x.id != cv.id
).length == 0;
- return noEmptyFields && noDuplicateShortCodes;
+ return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
}
getEditTooltip(cv: CustomEmojiViewForm) {
}
handleEmojiImageUrlChange(
- props: { form: EmojiForm; index: number; overrideValue: string | null },
+ {
+ form,
+ index,
+ overrideValue,
+ }: { form: EmojiForm; index: number; overrideValue: string | null },
event: any
) {
- const custom_emojis = [...props.form.state.customEmojis];
- const pagedIndex =
- (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- const item = {
- ...props.form.state.customEmojis[pagedIndex],
- image_url: props.overrideValue ?? event.target.value,
- changed: true,
- };
- custom_emojis[Number(pagedIndex)] = item;
- props.form.setState({ customEmojis: custom_emojis });
+ form.setState(prevState => {
+ const custom_emojis = [...form.state.customEmojis];
+ const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
+ const item = {
+ ...form.state.customEmojis[pagedIndex],
+ image_url: overrideValue ?? event.target.value,
+ changed: true,
+ };
+ custom_emojis[Number(pagedIndex)] = item;
+ return {
+ ...prevState,
+ customEmojis: prevState.customEmojis.map((ce, i) =>
+ i === pagedIndex
+ ? {
+ ...ce,
+ image_url: overrideValue ?? event.target.value,
+ changed: true,
+ loading: false,
+ }
+ : ce
+ ),
+ };
+ });
}
handleEmojiAltTextChange(
.split(" ")
.filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords));
- if (d.cv.id != 0) {
+ if (d.cv.id !== 0) {
d.i.props.onEdit({
id: d.cv.id,
category: d.cv.category,
handleAddEmojiClick(form: EmojiForm, event: any) {
event.preventDefault();
- const custom_emojis = [...form.state.customEmojis];
- const page =
- 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
- const item: CustomEmojiViewForm = {
- id: 0,
- shortcode: "",
- alt_text: "",
- category: "",
- image_url: "",
- keywords: "",
- changed: true,
- page: page,
- };
- custom_emojis.push(item);
- form.setState({ customEmojis: custom_emojis, page: page });
+ form.setState(prevState => {
+ const page =
+ 1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
+ const item: CustomEmojiViewForm = {
+ id: 0,
+ shortcode: "",
+ alt_text: "",
+ category: "",
+ image_url: "",
+ keywords: "",
+ changed: false,
+ page: page,
+ loading: false,
+ };
+
+ return {
+ ...prevState,
+ customEmojis: [...prevState.customEmojis, item],
+ page,
+ };
+ });
}
- handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
+ handleImageUpload(
+ { form, index }: { form: EmojiForm; index: number },
+ event: any
+ ) {
let file: any;
if (event.target) {
event.preventDefault();
file = event;
}
+ form.setState(prevState => ({
+ ...prevState,
+ customEmojis: prevState.customEmojis.map((cv, i) =>
+ i === index ? { ...cv, loading: true } : cv
+ ),
+ }));
+
HttpService.client.uploadImage({ image: file }).then(res => {
console.log("pictrs upload:");
console.log(res);
if (res.state === "success") {
if (res.data.msg === "ok") {
pictrsDeleteToast(file.name, res.data.delete_url as string);
- } else {
- toast(JSON.stringify(res), "danger");
- const hash = res.data.files?.at(0)?.file;
- const url = `${res.data.url}/${hash}`;
- props.form.handleEmojiImageUrlChange(
- { form: props.form, index: props.index, overrideValue: url },
+ form.handleEmojiImageUrlChange(
+ { form: form, index: index, overrideValue: res.data.url as string },
event
);
+ } else if (res.data.msg === "too_large") {
+ toast(I18NextService.i18n.t("upload_too_large"), "danger");
+ } else {
+ toast(JSON.stringify(res), "danger");
}
} else if (res.state === "failed") {
console.error(res.msg);