mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 17:42:25 -06:00 
			
		
		
		
	refactor custom emoji form fields
This commit is contained in:
		
					parent
					
						
							
								41f25653d1
							
						
					
				
			
			
				commit
				
					
						90fa9a3ce6
					
				
			
		
					 9 changed files with 325 additions and 176 deletions
				
			
		| 
						 | 
				
			
			@ -261,12 +261,16 @@ section.login {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
section.error {
 | 
			
		||||
	word-break: break-word;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	margin-bottom: 0.5rem;
 | 
			
		||||
 | 
			
		||||
	span {
 | 
			
		||||
		font-size: 2em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pre {
 | 
			
		||||
		border: 1px solid #ff000080;
 | 
			
		||||
		margin-left: 1em;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										133
									
								
								web/source/settings/admin/emoji/new-emoji.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								web/source/settings/admin/emoji/new-emoji.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
/*
 | 
			
		||||
	GoToSocial
 | 
			
		||||
	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
			
		||||
 | 
			
		||||
	This program is free software: you can redistribute it and/or modify
 | 
			
		||||
	it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
	the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
	(at your option) any later version.
 | 
			
		||||
 | 
			
		||||
	This program is distributed in the hope that it will be useful,
 | 
			
		||||
	but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
	GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
	You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Promise = require('bluebird');
 | 
			
		||||
const React = require("react");
 | 
			
		||||
 | 
			
		||||
const FakeToot = require("../../components/fake-toot");
 | 
			
		||||
const MutateButton = require("../../components/mutation-button");
 | 
			
		||||
 | 
			
		||||
const { 
 | 
			
		||||
	useTextInput,
 | 
			
		||||
	useFileInput
 | 
			
		||||
} = require("../../components/form");
 | 
			
		||||
 | 
			
		||||
const query = require("../../lib/query");
 | 
			
		||||
 | 
			
		||||
module.exports = function NewEmojiForm({emoji}) {
 | 
			
		||||
	const emojiCodes = React.useMemo(() => {
 | 
			
		||||
		return new Set(emoji.map((e) => e.shortcode));
 | 
			
		||||
	}, [emoji]);
 | 
			
		||||
 | 
			
		||||
	const [addEmoji, result] = query.useAddEmojiMutation();
 | 
			
		||||
 | 
			
		||||
	const [onFileChange, resetFile, {image, imageURL, imageInfo}] = useFileInput("image", {
 | 
			
		||||
		withPreview: true,
 | 
			
		||||
		maxSize: 50 * 1000
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const [onShortcodeChange, resetShortcode, {shortcode, setShortcode, shortcodeRef}] = useTextInput("shortcode", {
 | 
			
		||||
		validator: function validateShortcode(code) {
 | 
			
		||||
			return emojiCodes.has(code)
 | 
			
		||||
				? "Shortcode already in use"
 | 
			
		||||
				: "";
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	React.useEffect(() => {
 | 
			
		||||
		if (shortcode.length == 0) {
 | 
			
		||||
			if (image != undefined) {
 | 
			
		||||
				let [name, _ext] = image.name.split(".");
 | 
			
		||||
				setShortcode(name);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
	}, [image]);
 | 
			
		||||
 | 
			
		||||
	function uploadEmoji(e) {
 | 
			
		||||
		if (e) {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Promise.try(() => {
 | 
			
		||||
			return addEmoji({
 | 
			
		||||
				image,
 | 
			
		||||
				shortcode
 | 
			
		||||
			});
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			resetFile();
 | 
			
		||||
			resetShortcode();
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let emojiOrShortcode = `:${shortcode}:`;
 | 
			
		||||
 | 
			
		||||
	if (imageURL != undefined) {
 | 
			
		||||
		emojiOrShortcode = <img
 | 
			
		||||
			className="emoji"
 | 
			
		||||
			src={imageURL}
 | 
			
		||||
			title={`:${shortcode}:`}
 | 
			
		||||
			alt={shortcode}
 | 
			
		||||
		/>;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h2>Add new custom emoji</h2>
 | 
			
		||||
 | 
			
		||||
			<FakeToot>
 | 
			
		||||
				Look at this new custom emoji {emojiOrShortcode} isn't it cool?
 | 
			
		||||
			</FakeToot>
 | 
			
		||||
 | 
			
		||||
			<form onSubmit={uploadEmoji} className="form-flex">
 | 
			
		||||
				<div className="form-field file">
 | 
			
		||||
					<label className="file-input button" htmlFor="image">
 | 
			
		||||
						Browse
 | 
			
		||||
					</label>
 | 
			
		||||
					{imageInfo}
 | 
			
		||||
					<input
 | 
			
		||||
						className="hidden"
 | 
			
		||||
						type="file"
 | 
			
		||||
						id="image"
 | 
			
		||||
						name="Image"
 | 
			
		||||
						accept="image/png,image/gif"
 | 
			
		||||
						onChange={onFileChange}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div className="form-field text">
 | 
			
		||||
					<label htmlFor="shortcode">
 | 
			
		||||
						Shortcode, must be unique among the instance's local emoji
 | 
			
		||||
					</label>
 | 
			
		||||
					<input
 | 
			
		||||
						type="text"
 | 
			
		||||
						id="shortcode"
 | 
			
		||||
						name="Shortcode"
 | 
			
		||||
						ref={shortcodeRef}
 | 
			
		||||
						onChange={onShortcodeChange}
 | 
			
		||||
						value={shortcode}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
				
 | 
			
		||||
				<MutateButton text="Upload emoji" result={result}/>
 | 
			
		||||
			</form>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -18,14 +18,11 @@
 | 
			
		|||
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Promise = require("bluebird");
 | 
			
		||||
const React = require("react");
 | 
			
		||||
const {Link} = require("wouter");
 | 
			
		||||
const defaultValue = require('default-value');
 | 
			
		||||
const prettierBytes = require("prettier-bytes");
 | 
			
		||||
 | 
			
		||||
const FakeToot = require("../../components/fake-toot");
 | 
			
		||||
const MutateButton = require("../../components/mutation-button");
 | 
			
		||||
const NewEmojiForm = require("./new-emoji");
 | 
			
		||||
 | 
			
		||||
const query = require("../../lib/query");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +45,7 @@ module.exports = function EmojiOverview() {
 | 
			
		|||
				? "Loading..."
 | 
			
		||||
				: <>
 | 
			
		||||
					<EmojiList emoji={emoji}/>
 | 
			
		||||
					<NewEmoji emoji={emoji}/>
 | 
			
		||||
					<NewEmojiForm emoji={emoji}/>
 | 
			
		||||
				</>
 | 
			
		||||
			}
 | 
			
		||||
		</>
 | 
			
		||||
| 
						 | 
				
			
			@ -100,159 +97,3 @@ function EmojiCategory({category, entries}) {
 | 
			
		|||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useFileInput({withPreview, maxSize}) {
 | 
			
		||||
	const [file, setFile] = React.useState();
 | 
			
		||||
	const [imageURL, setImageURL] = React.useState();
 | 
			
		||||
	const [info, setInfo] = React.useState("no file selected");
 | 
			
		||||
 | 
			
		||||
	function onChange(e) {
 | 
			
		||||
		let file = e.target.files[0];
 | 
			
		||||
		setFile(file);
 | 
			
		||||
 | 
			
		||||
		URL.revokeObjectURL(imageURL);
 | 
			
		||||
 | 
			
		||||
		if (file != undefined) {
 | 
			
		||||
			if (withPreview) {
 | 
			
		||||
				setImageURL(URL.createObjectURL(file));
 | 
			
		||||
			}
 | 
			
		||||
	
 | 
			
		||||
			let size = prettierBytes(file.size);
 | 
			
		||||
			if (maxSize && file.size > maxSize) {
 | 
			
		||||
				size = <span className="error-text">{size}</span>;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			setInfo(<>
 | 
			
		||||
				{file.name} ({size})
 | 
			
		||||
			</>);
 | 
			
		||||
		} else {
 | 
			
		||||
			setInfo("no file selected");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function reset() {
 | 
			
		||||
		setFile();
 | 
			
		||||
		URL.revokeObjectURL(imageURL);
 | 
			
		||||
		setInfo("no file selected");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return [
 | 
			
		||||
		onChange,
 | 
			
		||||
		reset,
 | 
			
		||||
		{
 | 
			
		||||
			file,
 | 
			
		||||
			imageURL,
 | 
			
		||||
			info: <span>{info}</span>,
 | 
			
		||||
		}
 | 
			
		||||
	];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: change form field code, maybe look into redux-final-form or similar
 | 
			
		||||
// or evaluate if we even need to put most of this in the store
 | 
			
		||||
function NewEmoji({emoji}) {
 | 
			
		||||
	const emojiCodes = React.useMemo(() => {
 | 
			
		||||
		return new Set(emoji.map((e) => e.shortcode));
 | 
			
		||||
	}, [emoji]);
 | 
			
		||||
	const [addEmoji, result] = query.useAddEmojiMutation();
 | 
			
		||||
	const [onFileChange, resetFile, {file, imageURL, info}] = useFileInput({
 | 
			
		||||
		withPreview: true,
 | 
			
		||||
		maxSize: 50 * 1000
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const [shortcode, setShortcode] = React.useState("");
 | 
			
		||||
	const shortcodeRef = React.useRef(null);
 | 
			
		||||
 | 
			
		||||
	function onShortChange(e) {
 | 
			
		||||
		let input = e.target.value;
 | 
			
		||||
		setShortcode(input);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	React.useEffect(() => {
 | 
			
		||||
		if (emojiCodes.has(shortcode)) {
 | 
			
		||||
			shortcodeRef.current.setCustomValidity("Shortcode already in use");
 | 
			
		||||
		} else {
 | 
			
		||||
			shortcodeRef.current.setCustomValidity("");
 | 
			
		||||
		}
 | 
			
		||||
		shortcodeRef.current.reportValidity();
 | 
			
		||||
	}, [shortcode, shortcodeRef, emojiCodes]);
 | 
			
		||||
 | 
			
		||||
	React.useEffect(() => {
 | 
			
		||||
		if (shortcode.length == 0) {
 | 
			
		||||
			if (file != undefined) {
 | 
			
		||||
				let [name, _ext] = file.name.split(".");
 | 
			
		||||
				setShortcode(name);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
	}, [file]);
 | 
			
		||||
 | 
			
		||||
	function uploadEmoji(e) {
 | 
			
		||||
		if (e) {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Promise.try(() => {
 | 
			
		||||
			return addEmoji({
 | 
			
		||||
				image: file,
 | 
			
		||||
				shortcode
 | 
			
		||||
			});
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			resetFile();
 | 
			
		||||
			setShortcode("");
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let emojiOrShortcode = `:${shortcode}:`;
 | 
			
		||||
 | 
			
		||||
	if (imageURL != undefined) {
 | 
			
		||||
		emojiOrShortcode = <img
 | 
			
		||||
			className="emoji"
 | 
			
		||||
			src={imageURL}
 | 
			
		||||
			title={`:${shortcode}:`}
 | 
			
		||||
			alt={shortcode}
 | 
			
		||||
		/>;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h2>Add new custom emoji</h2>
 | 
			
		||||
 | 
			
		||||
			<FakeToot>
 | 
			
		||||
				Look at this new custom emoji {emojiOrShortcode} isn't it cool?
 | 
			
		||||
			</FakeToot>
 | 
			
		||||
 | 
			
		||||
			<form onSubmit={uploadEmoji} className="form-flex">
 | 
			
		||||
				<div className="form-field file">
 | 
			
		||||
					<label className="file-input button" htmlFor="image">
 | 
			
		||||
						Browse
 | 
			
		||||
					</label>
 | 
			
		||||
					{info}
 | 
			
		||||
					<input
 | 
			
		||||
						className="hidden"
 | 
			
		||||
						type="file"
 | 
			
		||||
						id="image"
 | 
			
		||||
						name="Image"
 | 
			
		||||
						accept="image/png,image/gif"
 | 
			
		||||
						onChange={onFileChange}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div className="form-field text">
 | 
			
		||||
					<label htmlFor="shortcode">
 | 
			
		||||
						Shortcode, must be unique among the instance's local emoji
 | 
			
		||||
					</label>
 | 
			
		||||
					<input
 | 
			
		||||
						type="text"
 | 
			
		||||
						id="shortcode"
 | 
			
		||||
						name="Shortcode"
 | 
			
		||||
						ref={shortcodeRef}
 | 
			
		||||
						onChange={onShortChange}
 | 
			
		||||
						value={shortcode}
 | 
			
		||||
					/>
 | 
			
		||||
				</div>
 | 
			
		||||
				
 | 
			
		||||
				<MutateButton text="Upload emoji" result={result}/>
 | 
			
		||||
			</form>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ module.exports = {
 | 
			
		|||
				field = (
 | 
			
		||||
					<>
 | 
			
		||||
						<label htmlFor={id} className="file-input button">Browse</label>
 | 
			
		||||
						<span>
 | 
			
		||||
						<span className="form-info">
 | 
			
		||||
							{file ? file.name : "no file selected"} {size}
 | 
			
		||||
						</span>
 | 
			
		||||
						{/* <a onClick={removeFile("header")}>remove</a> */}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										78
									
								
								web/source/settings/components/form/file.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web/source/settings/components/form/file.jsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
/*
 | 
			
		||||
	GoToSocial
 | 
			
		||||
	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
			
		||||
 | 
			
		||||
	This program is free software: you can redistribute it and/or modify
 | 
			
		||||
	it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
	the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
	(at your option) any later version.
 | 
			
		||||
 | 
			
		||||
	This program is distributed in the hope that it will be useful,
 | 
			
		||||
	but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
	GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
	You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const React = require("react");
 | 
			
		||||
const prettierBytes = require("prettier-bytes");
 | 
			
		||||
 | 
			
		||||
module.exports = function useFileInput({name, Name}, {
 | 
			
		||||
	withPreview,
 | 
			
		||||
	maxSize,
 | 
			
		||||
	initialInfo = "no file selected"
 | 
			
		||||
}) {
 | 
			
		||||
	const [file, setFile] = React.useState();
 | 
			
		||||
	const [imageURL, setImageURL] = React.useState();
 | 
			
		||||
	const [info, setInfo] = React.useState();
 | 
			
		||||
 | 
			
		||||
	function onChange(e) {
 | 
			
		||||
		let file = e.target.files[0];
 | 
			
		||||
		setFile(file);
 | 
			
		||||
 | 
			
		||||
		URL.revokeObjectURL(imageURL);
 | 
			
		||||
 | 
			
		||||
		if (file != undefined) {
 | 
			
		||||
			if (withPreview) {
 | 
			
		||||
				setImageURL(URL.createObjectURL(file));
 | 
			
		||||
			}
 | 
			
		||||
	
 | 
			
		||||
			let size = prettierBytes(file.size);
 | 
			
		||||
			if (maxSize && file.size > maxSize) {
 | 
			
		||||
				size = <span className="error-text">{size}</span>;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			setInfo(<>
 | 
			
		||||
				{file.name} ({size})
 | 
			
		||||
			</>);
 | 
			
		||||
		} else {
 | 
			
		||||
			setInfo();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function reset() {
 | 
			
		||||
		URL.revokeObjectURL(imageURL);
 | 
			
		||||
		setImageURL();
 | 
			
		||||
		setFile();
 | 
			
		||||
		setInfo();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return [
 | 
			
		||||
		onChange,
 | 
			
		||||
		reset,
 | 
			
		||||
		{
 | 
			
		||||
			[name]: file,
 | 
			
		||||
			[`${name}URL`]: imageURL,
 | 
			
		||||
			[`${name}Info`]: <span className="form-info">
 | 
			
		||||
				{info
 | 
			
		||||
					? info
 | 
			
		||||
					: initialInfo
 | 
			
		||||
				}
 | 
			
		||||
			</span>
 | 
			
		||||
		}
 | 
			
		||||
	];
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										36
									
								
								web/source/settings/components/form/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/source/settings/components/form/index.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
/*
 | 
			
		||||
	 GoToSocial
 | 
			
		||||
	 Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
			
		||||
 | 
			
		||||
	 This program is free software: you can redistribute it and/or modify
 | 
			
		||||
	 it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
	 the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
	 (at your option) any later version.
 | 
			
		||||
 | 
			
		||||
	 This program is distributed in the hope that it will be useful,
 | 
			
		||||
	 but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
	 GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
	 You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
	 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
function capitalizeFirst(str) {
 | 
			
		||||
	return str.slice(0,1).toUpperCase()+str.slice(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeHook(func) {
 | 
			
		||||
	return (name, ...args) => func({
 | 
			
		||||
		name,
 | 
			
		||||
		Name: capitalizeFirst(name)
 | 
			
		||||
	},
 | 
			
		||||
	...args);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
	useTextInput: makeHook(require("./text")),
 | 
			
		||||
	useFileInput: makeHook(require("./file"))
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										56
									
								
								web/source/settings/components/form/text.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/source/settings/components/form/text.jsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
/*
 | 
			
		||||
	GoToSocial
 | 
			
		||||
	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
			
		||||
 | 
			
		||||
	This program is free software: you can redistribute it and/or modify
 | 
			
		||||
	it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
	the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
	(at your option) any later version.
 | 
			
		||||
 | 
			
		||||
	This program is distributed in the hope that it will be useful,
 | 
			
		||||
	but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
	GNU Affero General Public License for more details.
 | 
			
		||||
 | 
			
		||||
	You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const React = require("react");
 | 
			
		||||
 | 
			
		||||
module.exports = function useTextInput({name, Name}, {validator} = {}) {
 | 
			
		||||
	const [text, setText] = React.useState("");
 | 
			
		||||
	const textRef = React.useRef(null);
 | 
			
		||||
 | 
			
		||||
	function onChange(e) {
 | 
			
		||||
		let input = e.target.value;
 | 
			
		||||
		setText(input);
 | 
			
		||||
 | 
			
		||||
		if (validator) {
 | 
			
		||||
			validator(input);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function reset() {
 | 
			
		||||
		setText("");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	React.useEffect(() => {
 | 
			
		||||
		if (validator) {
 | 
			
		||||
			textRef.current.setCustomValidity(validator(text));
 | 
			
		||||
			textRef.current.reportValidity();
 | 
			
		||||
		}
 | 
			
		||||
	}, [text, textRef, validator]);
 | 
			
		||||
 | 
			
		||||
	return [
 | 
			
		||||
		onChange,
 | 
			
		||||
		reset,
 | 
			
		||||
		{
 | 
			
		||||
			[name]: text,
 | 
			
		||||
			[`${name}Ref`]: textRef,
 | 
			
		||||
			[`set${Name}`]: setText
 | 
			
		||||
		}
 | 
			
		||||
	];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ module.exports = function MutateButton({text, result}) {
 | 
			
		|||
 | 
			
		||||
	return (<div>
 | 
			
		||||
		{result.error && 
 | 
			
		||||
			<div className="error">{result.error.status}: {result.error.error}</div>
 | 
			
		||||
			<section className="error">{result.error.status}: {result.error.data.error}</section>
 | 
			
		||||
		}
 | 
			
		||||
		<input
 | 
			
		||||
			className="button"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -336,19 +336,6 @@ section.with-sidebar > div {
 | 
			
		|||
			flex-direction: column;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
 | 
			
		||||
			div.form-field {
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				display: flex;
 | 
			
		||||
 | 
			
		||||
				span {
 | 
			
		||||
					flex: 1 1 auto;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					text-overflow: ellipsis;
 | 
			
		||||
					white-space: nowrap;
 | 
			
		||||
					padding: 0.3rem 0;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			h3 {
 | 
			
		||||
				margin-top: 0;
 | 
			
		||||
				margin-bottom: 0.5rem;
 | 
			
		||||
| 
						 | 
				
			
			@ -369,6 +356,20 @@ section.with-sidebar > div {
 | 
			
		|||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-field.file {
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
span.form-info {
 | 
			
		||||
	flex: 1 1 auto;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	padding: 0.3rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue