Files
Card-Collection-Manager-2-s…/card-collection-manager-2/src/components/templates/CreateEditModalTemplate.tsx
T
Sebastian Dine 28579efc80 initial commit
2022-12-02 14:37:07 +00:00

425 lines
16 KiB
TypeScript

import React, {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import ModalTemplate from "../templates/ModalTemplate";
import { SetTemplate, EntryTemplate } from "../../types";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import { VscClose } from "react-icons/vsc";
import IntegerInput from "../templates/IntegerInput";
import ConfirmationModal from "../modals/ConfirmationModal";
// Enum to control wether the modal is in "Create" or "Edit" mode.
export enum Mode {
Create,
Edit,
}
/**
* Configuration object to specify additional, game-specific binary attributes of an entry.
* The specified label will be displayed for a corresponding checkbox that represents
* this attribute. The accessKey is used to retrieve the current value of the attribute
* for an entry that is editied as well as to send a new/updated value of the attribute
* to the backend.
*/
type ExtraAttribute = {
label: string;
accessKey: string;
}
/**
* Template modal to create a new collection entries or edit existing ones.
* The modal needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
*
* * game - Identifier of the game for which this modal should create or edit entries.
* This is required to identify the correct collection and corresponding file system
* directories.
*
* * extraAttributes - List of additional, games-specific binary attributes beyond the standard binary
* attributes `signed` and `altered`
*
* * mode - Flag to specifiy if the modal should be in "Create new Entry" or "Edit existing Entry" node.
*
* * collection - Reference to the current entry collection.
* * setCollection - Function to set/update the collection list to add new entries or update existing ones.
*
* * selectedEntry - Reference to the currently selected entry that should be edited in case the modal is in `Edit` mode.
* * setSelectedEntry - Function to set/update the currently selected entry.
*
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
*/
const CreateEditModalTemplate: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
game: string;
extraAttributes: ExtraAttribute[]
collection: EntryTemplate[];
mode: Mode;
selectedEntry: EntryTemplate;
setCollection: Dispatch<SetStateAction<EntryTemplate[]>>;
setSelectedEntry: Dispatch<SetStateAction<EntryTemplate>>;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const [sets, setSets] = useState<SetTemplate[]>([]);
const [languages, setLanguages] = useState<string[]>([]);
const [conditions, setConditions] = useState<string[]>([]);
// confirmation modal visibilities
const [abortConfirmationModalVisibility, setAbortConfirmationModalVisiblity] = useState<boolean>(false);
// form input element refs
const nameRef = useRef<HTMLInputElement>(null);
const setRef = useRef<HTMLSelectElement>(null);
const setNoRef = useRef<HTMLInputElement>(null);
const languageRef = useRef<HTMLSelectElement>(null);
const conditionRef = useRef<HTMLSelectElement>(null);
const amountRef = useRef<HTMLInputElement>(null);
const signedRef = useRef<HTMLInputElement>(null);
const alteredRef = useRef<HTMLInputElement>(null);
const noteRef = useRef<HTMLInputElement>(null);
// dynamic creation of refs for extra attributes
let extraAttributesRefs = {};
props.extraAttributes.map(attribute => extraAttributesRefs[attribute.accessKey] = useRef<HTMLInputElement>(null));
// images as state variable for better handling
const [images, setImages] = useState<string[]>([]);
// The first time this modal gets rendered, it fetches language and condition
// informations from the backened.
useEffect(() => {
invoke("get_language_variants_json").then((result) => {
const obj = JSON.parse(result as string) as string[];
setLanguages(obj);
});
invoke("get_condition_variants_json").then((result) => {
const obj = JSON.parse(result as string) as string[];
setConditions(obj);
});
}, []);
// everytime the modal becomes visible in "Edit" mode, it populates
// its fields with the data from the selected entry.
useEffect(() => {
if (props.visible) {
// get set data. We need to do this each time the element
// becomes visible, because the user might have updated the set
// data in between.
invoke("get_sets", {game: props.game}).then((result) => {
const obj = JSON.parse(result as string) as SetTemplate[];
setSets(obj);
});
// populate fields with data in case of 'edit' mode
if (props.mode == Mode.Edit) {
nameRef.current!.value = props.selectedEntry.name;
setRef.current!.value = props.selectedEntry.set.id.toLowerCase();
setNoRef.current!.value = props.selectedEntry.setNo;
languageRef.current!.value = props.selectedEntry.language;
conditionRef.current!.value = props.selectedEntry.condition;
amountRef.current!.value = `${props.selectedEntry.amount}`;
noteRef.current!.value = props.selectedEntry.note;
signedRef.current!.checked = props.selectedEntry.signed;
alteredRef.current!.checked = props.selectedEntry.altered;
// populate dynamic extra attribute fields
props.extraAttributes.map(attribute => extraAttributesRefs[attribute.accessKey].current!.checked = props.selectedEntry[attribute.accessKey]);
setImages(props.selectedEntry.images);
}
} else {
// clear image state in any case the modal gets closed
setImages([]);
}
}, [props.visible]);
// handler when user aborts maintaining an entry by clicking the close icon
const onClose = () => {
// if "Create" mode, delete all temporary stored images
if (props.mode == Mode.Create) {
images.forEach((image) => {
invoke("delete_image", { image: image, game: props.game });
});
}
// if "Edit" mode, delete all newly added images
if (props.mode == Mode.Edit) {
images
.filter((image) => !props.selectedEntry.images.includes(image))
.forEach((image) => {
invoke("delete_image", { image: image, game: props.game });
});
}
props.setVisible(false);
};
// get temporary entry from the current values stored in all input fields
const getTempEntry = () => {
let cardEntry: EntryTemplate = {
id: props.mode == Mode.Create ? 0 : props.selectedEntry.id,
name: nameRef.current!.value,
set: sets.filter((set) => set.id === setRef.current!.value)[0],
setNo: setNoRef.current!.value,
language: languageRef.current!.value,
condition: conditionRef.current!.value,
amount: Number.parseInt(amountRef.current!.value),
altered: alteredRef.current!.checked,
signed: signedRef.current!.checked,
note: noteRef.current!.value,
images: images,
};
// get values of extra attributes
props.extraAttributes.map(attribute => cardEntry[attribute.accessKey] = extraAttributesRefs[attribute.accessKey].current!.checked);
return cardEntry;
};
// submit an entry to the backend based on the current values of all input fields.
// if "Edit" mode, the existing entry will be overwritten.
const submitEntry = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
let cardEntry = getTempEntry();
if (props.mode == Mode.Create) {
invoke("add_card", { obj: JSON.stringify(cardEntry), game: props.game })
.then((result) => {
cardEntry.id = Number.parseInt(JSON.parse(result as string));
props.setCollection(props.collection.concat(cardEntry));
props.setVisible(false);
})
.catch((reject) => console.log(reject));
}
if (props.mode == Mode.Edit) {
cardEntry.id = props.selectedEntry.id;
invoke("update_card", { obj: JSON.stringify(cardEntry), game: props.game })
.then(() => {
props.setCollection(
props.collection.map((entry) =>
entry.id == cardEntry.id ? cardEntry : entry
)
);
props.setSelectedEntry(cardEntry);
props.setVisible(false);
})
.catch((reject) => console.log(reject));
}
};
// trigger the image selection dialog, copy the selected image via the backend service and
// store the returned image name in the temporary list of image names.
const addImage = async () => {
// only add image if name field has a value
if (nameRef.current && nameRef.current.reportValidity()) {
const selected = await open({
multiple: true,
title: "Select image",
filters: [
{
name: "Image",
extensions: ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG"],
},
],
});
if (selected && Array.isArray(selected)) {
let cardEntry = getTempEntry();
for(let i = 0; i < selected.length; i++) {
const imgId = await invoke("copy_image", {
obj: JSON.stringify(cardEntry),
imgLocation: selected[i],
newEntry: props.mode == Mode.Create,
game: props.game
});
cardEntry.images.push(imgId as string);
}
setImages([].concat(cardEntry.images)); // using the concat with an empty array is a trick to trigger the rerender, otherwise the state does recognize to rerender
}
}
};
// Enable the connected image modal and start the display by the provided image index.
const displayImage = async (index: number) => {
props.setImageModalImages(images);
props.setImageModalImageIndex(index);
props.setImageModalVisible(true);
};
// Delete the image with the specified image name by calling the corresponding backend service
// and remove the image name from the temporary list.
const deleteImage = async (image: string) => {
invoke("delete_image", { image: image, game: props.game })
.then(() => {
setImages(images.filter((img) => img != image));
})
.catch((reject) => console.log(reject));
};
return (
<>
{props.visible ? (
<ModalTemplate
title={props.mode == Mode.Create ? "Create Entry" : "Edit Entry"}
onClickCloseIcon={() => setAbortConfirmationModalVisiblity(true)}
modalStyle="w-[60%] h-[70%] lg:w-[50%] xl:w-[35%] 2xl:w-[30%]"
>
<div className="relative flex justify-center items-center text-gray-600 mx-8">
<form onSubmit={(e) => submitEntry(e)}>
<div className="grid grid-settings-8 gap-x-4 gap-y-4">
<label className="text-sm col-span-1">Name</label>
<input
className="text-sm col-span-7 border-2"
type="text"
required={true}
autoFocus={true}
ref={nameRef}
/>
<label className="text-sm col-span-1">Set</label>
<select className="text-sm col-span-7 border-2" ref={setRef}>
{sets.map((set) => (
<option value={`${set.id}`}>{set.name}</option>
))}
</select>
<label className="text-sm col-span-1">Set No.</label>
<input type="number"
className="text-sm col-span-2 border-2"
defaultValue={""}
ref={setNoRef}
/>
<div className="col-span-5" />
<label className="text-sm col-span-1">Language</label>
<select
className="text-sm col-span-3 border-2"
ref={languageRef}
>
{languages.map((language) => (
<option value={language}>{language}</option>
))}
</select>
<div className="col-span-4" />
<label className="text-sm col-span-1">Condition</label>
<select
className="text-sm col-span-3 border-2"
ref={conditionRef}
>
{conditions.map((condition) => (
<option value={condition}>{condition}</option>
))}
</select>
<div className="col-span-4" />
<label className="text-sm col-span-1">Amount</label>
<IntegerInput
className="text-sm col-span-2 border-2"
required={true}
ref={amountRef}
/>
<div className="col-span-5" />
<div className="text-sm col-span-1" />
<div className="text-sm col-span-7 flex justify-between">
{/* fields for dynamic extra attributes */}
{props.extraAttributes.map(attribute =>
<div>
<input type="checkbox" ref={extraAttributesRefs[attribute.accessKey]} />
<label className="mx-2">{attribute.label}</label>
</div>
)}
{/* default attributes */}
<div>
<input type="checkbox" ref={signedRef} />
<label className="mx-2">Signed</label>
</div>
<div>
<input type="checkbox" ref={alteredRef} />
<label className="mx-2">Altered</label>
</div>
</div>
<label className="text-sm col-span-1">Note</label>
<input
className="text-sm col-span-7 border-2"
type="text"
ref={noteRef}
/>
<label className="text-sm col-span-1">Images</label>
<div className="text-sm col-span-7">
{images.map((value, index) => (
<div className="flex items-center">
<div
className="hover:bg-blue-50 cursor-pointer"
id={index.toString()}
onClick={() => displayImage(index)}
>
Image {index + 1}
</div>
<div
className="mx-4 cursor-pointer hover:scale-125"
onClick={() => deleteImage(value)}
>
<VscClose className="text-red-600 text-lg" />
</div>
</div>
))}
<button
type="button"
className="my-4"
onClick={() => addImage()}
>
Add
</button>
</div>
</div>
<div className="my-4 text-center">
<button type="submit">Submit</button>
</div>
</form>
</div>
</ModalTemplate>
) : (
""
)}
<ConfirmationModal
visible={abortConfirmationModalVisibility}
setVisible={setAbortConfirmationModalVisiblity}
title={
props.mode == Mode.Create
? "Abort Creating"
: "Abort Editing"
}
text={
props.mode == Mode.Create
? "Do you reall want to abort the current entry creation?"
: "Do you really want to abort the current entry editing?"
}
confirmAction={() => onClose()}
/>
</>
);
};
export default CreateEditModalTemplate;