[Misc] Add admin for (un)linking 3rd party accounts (#4198)
* Updated admin panel to allow the concept of unlinking accounts * Don't look too hard at this commit, nothing to see here * Admin stuff * Fixed linking and unlinking and updated menu options * Undid some changes and cleaned up some code * Updated some logic and added some comments * Updates to admin panel logic * Stupid promises everyone hates them and they deserver to die * Promise stuff still * Promises working thanks to Ydarissep on discord - pushing with debug code before it decides to stop working again * Removed debugging code * All discord functionality seems to be working here?? Not sure what happened but yay * Fixed up some bugs and code * Added registered date to the panel * Fixed and updated some minor error message related stuff * Minor changes * Fixed some minor bugs, made the save related errors have error codes, and added updated icons * Updated search field error * Missed a couple of things to push * Fixed linting and doc errors * Revert dev related code and clean up dev comments * Reverting utils * Updating front end to match back end from Pancakes' comments * make getFields and getInputFieldConfigs a single function of FormUiHandler * remove outdated doc * Apply suggestions from code review Moka review changes Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> * Added docs * eslint fixes * Fixed error not showing up in certain conditions --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: MokaStitcher <millennium.stitcher@gmail.com> Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> Co-authored-by: innerthunder <brandonerickson98@gmail.com>
This commit is contained in:
parent
a13550ec44
commit
a2419c4fc3
Binary file not shown.
After Width: | Height: | Size: 209 B |
Binary file not shown.
After Width: | Height: | Size: 219 B |
Binary file not shown.
After Width: | Height: | Size: 209 B |
Binary file not shown.
After Width: | Height: | Size: 219 B |
|
@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase {
|
||||||
this.loadImage("discord", "ui");
|
this.loadImage("discord", "ui");
|
||||||
this.loadImage("google", "ui");
|
this.loadImage("google", "ui");
|
||||||
this.loadImage("settings_icon", "ui");
|
this.loadImage("settings_icon", "ui");
|
||||||
|
this.loadImage("link_icon", "ui");
|
||||||
|
this.loadImage("unlink_icon", "ui");
|
||||||
|
|
||||||
this.loadImage("default_bg", "arenas");
|
this.loadImage("default_bg", "arenas");
|
||||||
// Load arena images
|
// Load arena images
|
||||||
|
|
|
@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene";
|
||||||
import { ModalConfig } from "./modal-ui-handler";
|
import { ModalConfig } from "./modal-ui-handler";
|
||||||
import { Mode } from "./ui";
|
import { Mode } from "./ui";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||||
import { Button } from "#app/enums/buttons";
|
import { Button } from "#app/enums/buttons";
|
||||||
|
import { TextStyle } from "./text";
|
||||||
|
|
||||||
export default class AdminUiHandler extends FormModalUiHandler {
|
export default class AdminUiHandler extends FormModalUiHandler {
|
||||||
|
|
||||||
|
private adminMode: AdminMode;
|
||||||
|
private adminResult: AdminSearchInfo;
|
||||||
|
private config: ModalConfig;
|
||||||
|
|
||||||
|
private readonly buttonGap = 10;
|
||||||
|
// http response from the server when a username isn't found in the server
|
||||||
|
private readonly httpUserNotFoundErrorCode: number = 404;
|
||||||
|
private readonly ERR_REQUIRED_FIELD = (field: string) => {
|
||||||
|
if (field === "username") {
|
||||||
|
return `${Utils.formatText(field)} is required`;
|
||||||
|
} else {
|
||||||
|
return `${Utils.formatText(field)} Id is required`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
|
||||||
|
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {
|
||||||
|
return `Username and ${service} successfully ${mode.toLowerCase()}ed`;
|
||||||
|
};
|
||||||
|
private readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";
|
||||||
|
private readonly ERR_GENERIC_ERROR: string = "There was an error";
|
||||||
|
|
||||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||||
super(scene, mode);
|
super(scene, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup(): void {
|
override getModalTitle(): string {
|
||||||
super.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
getModalTitle(config?: ModalConfig): string {
|
|
||||||
return "Admin panel";
|
return "Admin panel";
|
||||||
}
|
}
|
||||||
|
|
||||||
getFields(config?: ModalConfig): string[] {
|
override getWidth(): number {
|
||||||
return [ "Username", "Discord ID" ];
|
return this.adminMode === AdminMode.ADMIN ? 180 : 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWidth(config?: ModalConfig): number {
|
override getMargin(): [number, number, number, number] {
|
||||||
return 160;
|
return [ 0, 0, 0, 0 ];
|
||||||
}
|
}
|
||||||
|
|
||||||
getMargin(config?: ModalConfig): [number, number, number, number] {
|
override getButtonLabels(): string[] {
|
||||||
return [ 0, 0, 48, 0 ];
|
switch (this.adminMode) {
|
||||||
|
case AdminMode.LINK:
|
||||||
|
return [ "Link Account", "Cancel" ];
|
||||||
|
case AdminMode.SEARCH:
|
||||||
|
return [ "Find account", "Cancel" ];
|
||||||
|
case AdminMode.ADMIN:
|
||||||
|
return [ "Back to search", "Cancel" ];
|
||||||
|
default:
|
||||||
|
return [ "Activate ADMIN", "Cancel" ];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonLabels(config?: ModalConfig): string[] {
|
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||||
return [ "Link account", "Cancel" ];
|
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||||
|
switch (this.adminMode) {
|
||||||
|
case AdminMode.LINK:
|
||||||
|
inputFieldConfigs.push( { label: "Username" });
|
||||||
|
inputFieldConfigs.push( { label: "Discord ID" });
|
||||||
|
break;
|
||||||
|
case AdminMode.SEARCH:
|
||||||
|
inputFieldConfigs.push( { label: "Username" });
|
||||||
|
break;
|
||||||
|
case AdminMode.ADMIN:
|
||||||
|
const adminResult = this.adminResult ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" };
|
||||||
|
// Discord and Google ID fields that are not empty get locked, other fields are all locked
|
||||||
|
inputFieldConfigs.push( { label: "Username", isReadOnly: true });
|
||||||
|
inputFieldConfigs.push( { label: "Discord ID", isReadOnly: adminResult.discordId !== "" });
|
||||||
|
inputFieldConfigs.push( { label: "Google ID", isReadOnly: adminResult.googleId !== "" });
|
||||||
|
inputFieldConfigs.push( { label: "Last played", isReadOnly: true });
|
||||||
|
inputFieldConfigs.push( { label: "Registered", isReadOnly: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return inputFieldConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
processInput(button: Button): boolean {
|
processInput(button: Button): boolean {
|
||||||
|
@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
show(args: any[]): boolean {
|
show(args: any[]): boolean {
|
||||||
|
this.config = args[0] as ModalConfig; // config
|
||||||
|
this.adminMode = args[1] as AdminMode; // admin mode
|
||||||
|
this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; // admin result, if any
|
||||||
|
const isMessageError = args[3]; // is the message shown a success or error
|
||||||
|
|
||||||
|
const fields = this.getInputFieldConfigs();
|
||||||
|
const hasTitle = !!this.getModalTitle();
|
||||||
|
|
||||||
|
this.updateFields(fields, hasTitle);
|
||||||
|
this.updateContainer(this.config);
|
||||||
|
|
||||||
|
const labels = this.getButtonLabels();
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
this.buttonLabels[i].setText(labels[i]); // sets the label text
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically
|
||||||
|
if (isMessageError) {
|
||||||
|
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||||
|
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||||
|
} else {
|
||||||
|
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN));
|
||||||
|
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true));
|
||||||
|
}
|
||||||
|
|
||||||
if (super.show(args)) {
|
if (super.show(args)) {
|
||||||
const config = args[0] as ModalConfig;
|
this.populateFields(this.adminMode, this.adminResult);
|
||||||
const originalSubmitAction = this.submitAction;
|
const originalSubmitAction = this.submitAction;
|
||||||
this.submitAction = (_) => {
|
this.submitAction = (_) => {
|
||||||
this.submitAction = originalSubmitAction;
|
this.submitAction = originalSubmitAction;
|
||||||
|
const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later
|
||||||
|
const validFields = this.areFieldsValid(this.adminMode);
|
||||||
|
if (validFields.error) {
|
||||||
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
|
||||||
|
return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true);
|
||||||
|
}
|
||||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||||
const onFail = error => {
|
if (this.adminMode === AdminMode.LINK) {
|
||||||
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() }));
|
this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord
|
||||||
this.scene.ui.playError();
|
|
||||||
};
|
|
||||||
if (!this.inputs[0].text) {
|
|
||||||
return onFail("Username is required");
|
|
||||||
}
|
|
||||||
if (!this.inputs[1].text) {
|
|
||||||
return onFail("Discord Id is required");
|
|
||||||
}
|
|
||||||
Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (response.error) {
|
||||||
console.error(response);
|
return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind
|
||||||
|
} else {
|
||||||
|
return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success
|
||||||
}
|
}
|
||||||
this.inputs[0].setText("");
|
|
||||||
this.inputs[1].setText("");
|
|
||||||
this.scene.ui.revertMode();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
this.scene.ui.revertMode();
|
|
||||||
});
|
});
|
||||||
|
} else if (this.adminMode === AdminMode.SEARCH) {
|
||||||
|
this.adminSearch(adminSearchResult) // admin search for username
|
||||||
|
.then(response => {
|
||||||
|
if (response.error) {
|
||||||
|
return this.showMessage(response.errorType, adminSearchResult, true); // failure
|
||||||
|
}
|
||||||
|
this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success
|
||||||
|
});
|
||||||
|
} else if (this.adminMode === AdminMode.ADMIN) {
|
||||||
|
this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) {
|
||||||
|
this.scene.ui.setMode(Mode.ADMIN, Object.assign(this.config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError);
|
||||||
|
if (isError) {
|
||||||
|
this.scene.ui.playError();
|
||||||
|
} else {
|
||||||
|
this.scene.ui.playSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult}
|
||||||
|
* to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord.
|
||||||
|
* For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the
|
||||||
|
* appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result
|
||||||
|
*/
|
||||||
|
private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) {
|
||||||
|
switch (adminMode) {
|
||||||
|
case AdminMode.LINK:
|
||||||
|
this.inputs[0].setText(adminResult.username);
|
||||||
|
this.inputs[1].setText(adminResult.discordId);
|
||||||
|
break;
|
||||||
|
case AdminMode.SEARCH:
|
||||||
|
this.inputs[0].setText(adminResult.username);
|
||||||
|
break;
|
||||||
|
case AdminMode.ADMIN:
|
||||||
|
Object.keys(adminResult).forEach((aR, i) => {
|
||||||
|
this.inputs[i].setText(adminResult[aR]);
|
||||||
|
if (aR === "discordId" || aR === "googleId") { // this is here to add the icons for linking/unlinking of google/discord IDs
|
||||||
|
const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice");
|
||||||
|
const img = this.scene.add.image(this.inputContainers[i].x + nineSlice!.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice!.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon");
|
||||||
|
img.setName(`adminBtn_${aR}`);
|
||||||
|
img.setOrigin(0.5, 0.5);
|
||||||
|
img.setInteractive();
|
||||||
|
img.on("pointerdown", () => {
|
||||||
|
const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly
|
||||||
|
const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service
|
||||||
|
const validFields = this.areFieldsValid(this.adminMode, service);
|
||||||
|
if (validFields.error) {
|
||||||
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
|
||||||
|
return this.showMessage(validFields.errorMessage ?? "", adminResult, true);
|
||||||
|
}
|
||||||
|
this.adminLinkUnlink(this.convertInputsToAdmin(), service, mode).then(response => { // attempts to link/unlink depending on the service
|
||||||
|
if (response.error) {
|
||||||
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||||
|
return this.showMessage(response.errorType, adminResult, true); // fail
|
||||||
|
} else { // success, reload panel with new results
|
||||||
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||||
|
this.adminSearch(adminResult)
|
||||||
|
.then(response => {
|
||||||
|
if (response.error) {
|
||||||
|
return this.showMessage(response.errorType, adminResult, true);
|
||||||
|
}
|
||||||
|
return this.showMessage(this.SUCCESS_SERVICE_MODE(service, mode), response.adminSearchResult ?? adminResult, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.addInteractionHoverEffect(img);
|
||||||
|
this.modalContainer.add(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private areFieldsValid(adminMode: AdminMode, service?: string): { error: boolean; errorMessage?: string; } {
|
||||||
|
switch (adminMode) {
|
||||||
|
case AdminMode.LINK:
|
||||||
|
if (!this.inputs[0].text) { // username missing from link panel
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
errorMessage: this.ERR_REQUIRED_FIELD("username")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!this.inputs[1].text) { // discordId missing from linking panel
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
errorMessage: this.ERR_REQUIRED_FIELD("discord")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AdminMode.SEARCH:
|
||||||
|
if (!this.inputs[0].text) { // username missing from search panel
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
errorMessage: this.ERR_REQUIRED_FIELD("username")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AdminMode.ADMIN:
|
||||||
|
if (!this.inputs[1].text && service === "discord") { // discordId missing from admin panel
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
errorMessage: this.ERR_REQUIRED_FIELD(service)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
errorMessage: this.ERR_REQUIRED_FIELD(service)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertInputsToAdmin(): AdminSearchInfo {
|
||||||
|
return {
|
||||||
|
username: this.inputs[0]?.node ? this.inputs[0].text : "",
|
||||||
|
discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "",
|
||||||
|
googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "",
|
||||||
|
lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "",
|
||||||
|
registered: this.inputs[4]?.node ? this.inputs[4]?.text : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async adminSearch(adminSearchResult: AdminSearchInfo) {
|
||||||
|
try {
|
||||||
|
const adminInfo = await Utils.apiFetch(`admin/account/adminSearch?username=${encodeURIComponent(adminSearchResult.username)}`, true);
|
||||||
|
if (!adminInfo.ok) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
|
||||||
|
return { adminSearchResult: adminSearchResult, error: true, errorType: adminInfo.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
|
||||||
|
} else { // success
|
||||||
|
const adminInfoJson: AdminSearchInfo = await adminInfo.json();
|
||||||
|
return { adminSearchResult: adminInfoJson, error: false };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return { error: true, errorType: err };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) {
|
||||||
|
try {
|
||||||
|
const response = await Utils.apiPost(`admin/account/${service}${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&${service}Id=${encodeURIComponent(service === "discord" ? adminSearchResult.discordId : adminSearchResult.googleId)}`, "application/x-www-form-urlencoded", true);
|
||||||
|
if (!response.ok) { // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
|
||||||
|
return { adminSearchResult: adminSearchResult, error: true, errorType: response.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
|
||||||
|
} else { // success!
|
||||||
|
return { adminSearchResult: adminSearchResult, error: false };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return { error: true, errorType: err };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) {
|
||||||
|
mode = mode ?? AdminMode.ADMIN;
|
||||||
|
this.scene.ui.setMode(Mode.ADMIN, {
|
||||||
|
buttonActions: [
|
||||||
|
// we double revert here and below to go back 2 layers of menus
|
||||||
|
() => {
|
||||||
|
this.scene.ui.revertMode();
|
||||||
|
this.scene.ui.revertMode();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.scene.ui.revertMode();
|
||||||
|
this.scene.ui.revertMode();
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, mode, adminSearchResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
super.clear();
|
super.clear();
|
||||||
|
|
||||||
|
// this is used to remove the existing fields on the admin panel so they can be updated
|
||||||
|
|
||||||
|
const itemsToRemove: string[] = [ "formLabel", "adminBtn" ]; // this is the start of the names for each element we want to remove
|
||||||
|
const removeArray: any[] = [];
|
||||||
|
const mC = this.modalContainer.list;
|
||||||
|
for (let i = mC.length - 1; i >= 0; i--) {
|
||||||
|
/* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels
|
||||||
|
* this means that anything with, for example, "formLabel", will be true.
|
||||||
|
* It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText
|
||||||
|
* and if either of these conditions are met, the element is destroyed.
|
||||||
|
*/
|
||||||
|
if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && (mC[i] as Phaser.GameObjects.Container).list.find(m => m.type === "rexInputText"))) {
|
||||||
|
removeArray.push(mC[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (removeArray.length > 0) {
|
||||||
|
this.modalContainer.remove(removeArray.pop(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AdminMode {
|
||||||
|
LINK,
|
||||||
|
SEARCH,
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdminModeName(adminMode: AdminMode): string {
|
||||||
|
switch (adminMode) {
|
||||||
|
case AdminMode.LINK:
|
||||||
|
return "Link";
|
||||||
|
case AdminMode.SEARCH:
|
||||||
|
return "Search";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdminSearchInfo {
|
||||||
|
username: string;
|
||||||
|
discordId: string;
|
||||||
|
googleId: string;
|
||||||
|
lastLoggedIn: string;
|
||||||
|
registered: string;
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { TextStyle, addTextInputObject, addTextObject } from "./text";
|
||||||
import { WindowVariant, addWindow } from "./ui-theme";
|
import { WindowVariant, addWindow } from "./ui-theme";
|
||||||
import InputText from "phaser3-rex-plugins/plugins/inputtext";
|
import InputText from "phaser3-rex-plugins/plugins/inputtext";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import i18next from "i18next";
|
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
|
|
||||||
export interface FormModalConfig extends ModalConfig {
|
export interface FormModalConfig extends ModalConfig {
|
||||||
|
@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||||
protected errorMessage: Phaser.GameObjects.Text;
|
protected errorMessage: Phaser.GameObjects.Text;
|
||||||
protected submitAction: Function | null;
|
protected submitAction: Function | null;
|
||||||
protected tween: Phaser.Tweens.Tween;
|
protected tween: Phaser.Tweens.Tween;
|
||||||
|
protected formLabels: Phaser.GameObjects.Text[];
|
||||||
|
|
||||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||||
super(scene, mode);
|
super(scene, mode);
|
||||||
|
@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
this.inputContainers = [];
|
this.inputContainers = [];
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
|
this.formLabels = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getFields(): string[];
|
/**
|
||||||
|
* Get configuration for all fields that should be part of the modal
|
||||||
|
* Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view
|
||||||
|
* @returns array of {@linkcode InputFieldConfig}
|
||||||
|
*/
|
||||||
|
abstract getInputFieldConfigs(): InputFieldConfig[];
|
||||||
|
|
||||||
getHeight(config?: ModalConfig): number {
|
getHeight(config?: ModalConfig): number {
|
||||||
return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
|
return 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReadableErrorMessage(error: string): string {
|
getReadableErrorMessage(error: string): string {
|
||||||
|
@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||||
setup(): void {
|
setup(): void {
|
||||||
super.setup();
|
super.setup();
|
||||||
|
|
||||||
const fields = this.getFields();
|
const config = this.getInputFieldConfigs();
|
||||||
|
|
||||||
const hasTitle = !!this.getModalTitle();
|
const hasTitle = !!this.getModalTitle();
|
||||||
|
|
||||||
fields.forEach((field, f) => {
|
if (config.length >= 1) {
|
||||||
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT);
|
this.updateFields(config, hasTitle);
|
||||||
|
}
|
||||||
|
|
||||||
this.modalContainer.add(label);
|
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
|
||||||
|
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||||
|
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||||
|
this.errorMessage.setVisible(false);
|
||||||
|
this.modalContainer.add(this.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) {
|
||||||
|
this.inputContainers = [];
|
||||||
|
this.inputs = [];
|
||||||
|
this.formLabels = [];
|
||||||
|
fieldsConfig.forEach((config, f) => {
|
||||||
|
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT);
|
||||||
|
label.name = "formLabel" + f;
|
||||||
|
|
||||||
|
this.formLabels.push(label);
|
||||||
|
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
|
||||||
|
|
||||||
const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
|
const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
|
||||||
inputContainer.setVisible(false);
|
inputContainer.setVisible(false);
|
||||||
|
|
||||||
const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
|
const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
|
||||||
|
|
||||||
const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword"));
|
const isPassword = config?.isPassword;
|
||||||
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 });
|
const isReadOnly = config?.isReadOnly;
|
||||||
|
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly });
|
||||||
input.setOrigin(0, 0);
|
input.setOrigin(0, 0);
|
||||||
|
|
||||||
inputContainer.add(inputBg);
|
inputContainer.add(inputBg);
|
||||||
inputContainer.add(input);
|
inputContainer.add(input);
|
||||||
this.modalContainer.add(inputContainer);
|
|
||||||
|
|
||||||
this.inputContainers.push(inputContainer);
|
this.inputContainers.push(inputContainer);
|
||||||
|
this.modalContainer.add(inputContainer);
|
||||||
|
|
||||||
this.inputs.push(input);
|
this.inputs.push(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
|
|
||||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
|
||||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
|
||||||
this.errorMessage.setVisible(false);
|
|
||||||
this.modalContainer.add(this.errorMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show(args: any[]): boolean {
|
show(args: any[]): boolean {
|
||||||
|
@ -149,3 +168,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InputFieldConfig {
|
||||||
|
label: string,
|
||||||
|
isPassword?: boolean,
|
||||||
|
isReadOnly?: boolean
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||||
import { ModalConfig } from "./modal-ui-handler";
|
import { ModalConfig } from "./modal-ui-handler";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { Mode } from "./ui";
|
import { Mode } from "./ui";
|
||||||
|
@ -75,10 +75,6 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||||
return i18next.t("menu:login");
|
return i18next.t("menu:login");
|
||||||
}
|
}
|
||||||
|
|
||||||
override getFields(_config?: ModalConfig): string[] {
|
|
||||||
return [ i18next.t("menu:username"), i18next.t("menu:password") ];
|
|
||||||
}
|
|
||||||
|
|
||||||
override getWidth(_config?: ModalConfig): number {
|
override getWidth(_config?: ModalConfig): number {
|
||||||
return 160;
|
return 160;
|
||||||
}
|
}
|
||||||
|
@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||||
case this.ERR_PASSWORD_MATCH:
|
case this.ERR_PASSWORD_MATCH:
|
||||||
return i18next.t("menu:unmatchingPassword");
|
return i18next.t("menu:unmatchingPassword");
|
||||||
case this.ERR_NO_SAVES:
|
case this.ERR_NO_SAVES:
|
||||||
return i18next.t("menu:noSaves");
|
return "P01: " + i18next.t("menu:noSaves");
|
||||||
case this.ERR_TOO_MANY_SAVES:
|
case this.ERR_TOO_MANY_SAVES:
|
||||||
return i18next.t("menu:tooManySaves");
|
return "P02: " + i18next.t("menu:tooManySaves");
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.getReadableErrorMessage(error);
|
return super.getReadableErrorMessage(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||||
|
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||||
|
inputFieldConfigs.push({ label: i18next.t("menu:username") });
|
||||||
|
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
|
||||||
|
return inputFieldConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
override show(args: any[]): boolean {
|
override show(args: any[]): boolean {
|
||||||
if (super.show(args)) {
|
if (super.show(args)) {
|
||||||
|
|
||||||
|
@ -164,7 +167,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||||
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown"));
|
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private processExternalProvider(config: ModalConfig) : void {
|
private processExternalProvider(config: ModalConfig): void {
|
||||||
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
|
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
|
||||||
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length);
|
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length);
|
||||||
this.externalPartyTitle.setVisible(true);
|
this.externalPartyTitle.setVisible(true);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type";
|
||||||
import BgmBar from "#app/ui/bgm-bar";
|
import BgmBar from "#app/ui/bgm-bar";
|
||||||
import AwaitableUiHandler from "./awaitable-ui-handler";
|
import AwaitableUiHandler from "./awaitable-ui-handler";
|
||||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||||
|
import { AdminMode, getAdminModeName } from "./admin-ui-handler";
|
||||||
|
|
||||||
enum MenuOptions {
|
enum MenuOptions {
|
||||||
GAME_SETTINGS,
|
GAME_SETTINGS,
|
||||||
|
@ -386,17 +387,42 @@ export default class MenuUiHandler extends MessageUiHandler {
|
||||||
if (!bypassLogin && loggedInUser?.hasAdminRole) {
|
if (!bypassLogin && loggedInUser?.hasAdminRole) {
|
||||||
communityOptions.push({
|
communityOptions.push({
|
||||||
label: "Admin",
|
label: "Admin",
|
||||||
|
handler: () => {
|
||||||
|
|
||||||
|
const skippedAdminModes: AdminMode[] = [ AdminMode.ADMIN ]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN
|
||||||
|
const options: OptionSelectItem[] = [];
|
||||||
|
Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use
|
||||||
|
options.push({
|
||||||
|
label: getAdminModeName(mode as AdminMode),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
ui.playSelect();
|
ui.playSelect();
|
||||||
ui.setOverlayMode(Mode.ADMIN, {
|
ui.setOverlayMode(Mode.ADMIN, {
|
||||||
buttonActions: [
|
buttonActions: [
|
||||||
|
// we double revert here and below to go back 2 layers of menus
|
||||||
() => {
|
() => {
|
||||||
ui.revertMode();
|
ui.revertMode();
|
||||||
|
ui.revertMode();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
ui.revertMode();
|
ui.revertMode();
|
||||||
|
ui.revertMode();
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}, mode); // mode is our AdminMode enum
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
label: "Cancel",
|
||||||
|
handler: () => {
|
||||||
|
ui.revertMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
|
||||||
|
options: options,
|
||||||
|
delay: 0
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler {
|
||||||
protected titleText: Phaser.GameObjects.Text;
|
protected titleText: Phaser.GameObjects.Text;
|
||||||
protected buttonContainers: Phaser.GameObjects.Container[];
|
protected buttonContainers: Phaser.GameObjects.Container[];
|
||||||
protected buttonBgs: Phaser.GameObjects.NineSlice[];
|
protected buttonBgs: Phaser.GameObjects.NineSlice[];
|
||||||
|
protected buttonLabels: Phaser.GameObjects.Text[];
|
||||||
|
|
||||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||||
super(scene, mode);
|
super(scene, mode);
|
||||||
|
|
||||||
this.buttonContainers = [];
|
this.buttonContainers = [];
|
||||||
this.buttonBgs = [];
|
this.buttonBgs = [];
|
||||||
|
this.buttonLabels = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getModalTitle(config?: ModalConfig): string;
|
abstract getModalTitle(config?: ModalConfig): string;
|
||||||
|
@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler {
|
||||||
|
|
||||||
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
|
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
|
||||||
|
|
||||||
|
this.buttonLabels.push(buttonLabel);
|
||||||
this.buttonBgs.push(buttonBg);
|
this.buttonBgs.push(buttonBg);
|
||||||
this.buttonContainers.push(buttonContainer);
|
this.buttonContainers.push(buttonContainer);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||||
import { ModalConfig } from "./modal-ui-handler";
|
import { ModalConfig } from "./modal-ui-handler";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { Mode } from "./ui";
|
import { Mode } from "./ui";
|
||||||
|
@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||||
return i18next.t("menu:register");
|
return i18next.t("menu:register");
|
||||||
}
|
}
|
||||||
|
|
||||||
getFields(config?: ModalConfig): string[] {
|
|
||||||
return [ i18next.t("menu:username"), i18next.t("menu:password"), i18next.t("menu:confirmPassword") ];
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidth(config?: ModalConfig): number {
|
getWidth(config?: ModalConfig): number {
|
||||||
return 160;
|
return 160;
|
||||||
}
|
}
|
||||||
|
@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||||
return super.getReadableErrorMessage(error);
|
return super.getReadableErrorMessage(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||||
|
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||||
|
inputFieldConfigs.push({ label: i18next.t("menu:username") });
|
||||||
|
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
|
||||||
|
inputFieldConfigs.push({ label: i18next.t("menu:confirmPassword"), isPassword: true });
|
||||||
|
return inputFieldConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
setup(): void {
|
setup(): void {
|
||||||
super.setup();
|
super.setup();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||||
import { ModalConfig } from "./modal-ui-handler";
|
import { ModalConfig } from "./modal-ui-handler";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { PlayerPokemon } from "#app/field/pokemon";
|
import { PlayerPokemon } from "#app/field/pokemon";
|
||||||
|
@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
|
||||||
return i18next.t("menu:renamePokemon");
|
return i18next.t("menu:renamePokemon");
|
||||||
}
|
}
|
||||||
|
|
||||||
getFields(config?: ModalConfig): string[] {
|
|
||||||
return [ i18next.t("menu:nickname") ];
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidth(config?: ModalConfig): number {
|
getWidth(config?: ModalConfig): number {
|
||||||
return 160;
|
return 160;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
|
||||||
return super.getReadableErrorMessage(error);
|
return super.getReadableErrorMessage(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||||
|
return [{ label: i18next.t("menu:nickname") }];
|
||||||
|
}
|
||||||
|
|
||||||
show(args: any[]): boolean {
|
show(args: any[]): boolean {
|
||||||
if (super.show(args)) {
|
if (super.show(args)) {
|
||||||
const config = args[0] as ModalConfig;
|
const config = args[0] as ModalConfig;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||||
import { ModalConfig } from "./modal-ui-handler";
|
import { ModalConfig } from "./modal-ui-handler";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { PlayerPokemon } from "#app/field/pokemon";
|
import { PlayerPokemon } from "#app/field/pokemon";
|
||||||
|
@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
|
||||||
return "Test Dialogue";
|
return "Test Dialogue";
|
||||||
}
|
}
|
||||||
|
|
||||||
getFields(config?: ModalConfig): string[] {
|
|
||||||
return [ "Dialogue" ];
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidth(config?: ModalConfig): number {
|
getWidth(config?: ModalConfig): number {
|
||||||
return 300;
|
return 300;
|
||||||
}
|
}
|
||||||
|
@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
|
||||||
return super.getReadableErrorMessage(error);
|
return super.getReadableErrorMessage(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||||
|
return [{ label: "Dialogue" }];
|
||||||
|
}
|
||||||
|
|
||||||
show(args: any[]): boolean {
|
show(args: any[]): boolean {
|
||||||
const ui = this.getUi();
|
const ui = this.getUi();
|
||||||
|
const hasTitle = !!this.getModalTitle();
|
||||||
|
this.updateFields(this.getInputFieldConfigs(), hasTitle);
|
||||||
|
this.updateContainer(args[0] as ModalConfig);
|
||||||
const input = this.inputs[0];
|
const input = this.inputs[0];
|
||||||
input.setMaxLength(255);
|
input.setMaxLength(255);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue