From 013d85328dd958d5d4fc897817ac03cb16a8b897 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 21 Jul 2023 17:19:07 +0800 Subject: ... --- FrontEnd/src/views/common/dialog/Dialog.css | 20 ++++++++++++++------ FrontEnd/src/views/common/dialog/Dialog.tsx | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) (limited to 'FrontEnd/src/views/common/dialog') diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css index 21ea52fc..108bd823 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.css +++ b/FrontEnd/src/views/common/dialog/Dialog.css @@ -5,14 +5,22 @@ top: 0; right: 0; bottom: 0; - background-color: rgba(255, 255, 255, 0.92); - display: flex; padding: 2em; - overflow: auto; } +.cru-dialog-background { + position: absolute; + z-index: -1; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: var(--cru-surface-dim-color); + opacity: 0.8; +} + .cru-dialog-container { max-width: 100%; min-width: 30vw; @@ -22,7 +30,7 @@ border: var(--cru-primary-color) 1px solid; border-radius: 5px; padding: 1.5em; - background-color: white; + background-color: var(--cru-surface-color); } .cru-dialog-bottom-area { @@ -30,7 +38,7 @@ justify-content: flex-end; } -.cru-dialog-bottom-area > * { +.cru-dialog-bottom-area>* { margin: 0 0.5em; } @@ -52,4 +60,4 @@ transform: scale(0, 0); opacity: 0; transform-origin: center; -} +} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx index 923c636b..79a31954 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.tsx +++ b/FrontEnd/src/views/common/dialog/Dialog.tsx @@ -38,6 +38,7 @@ export default function Dialog(props: DialogProps) { } } > +
e.stopPropagation()} -- cgit v1.2.3 From ae1b296b5e967d1e329f5a1e6165ca0f05dce0cb Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 21 Jul 2023 17:25:01 +0800 Subject: ... --- FrontEnd/src/pages/setting/index.css | 1 - FrontEnd/src/views/common/Card.css | 4 ---- FrontEnd/src/views/common/dialog/Dialog.css | 2 +- FrontEnd/src/views/common/dialog/Dialog.tsx | 20 +++++++++++++++----- 4 files changed, 16 insertions(+), 11 deletions(-) (limited to 'FrontEnd/src/views/common/dialog') diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css index 9458d937..d9cba24f 100644 --- a/FrontEnd/src/pages/setting/index.css +++ b/FrontEnd/src/pages/setting/index.css @@ -36,7 +36,6 @@ .setting-item-container:hover { background-color: var(--cru-key-container-1-color); - border-bottom-color: var(--cru-key-container-1-color); } .setting-item-label-sub { diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css index 5b3dbbe9..f4cdc31c 100644 --- a/FrontEnd/src/views/common/Card.css +++ b/FrontEnd/src/views/common/Card.css @@ -5,7 +5,3 @@ border-color: var(--cru-key-container-color); transition: all 0.3s; } - -.cru-card:hover { - border-color: var(--cru-key-1-color); -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css index 108bd823..99e1a516 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.css +++ b/FrontEnd/src/views/common/dialog/Dialog.css @@ -27,7 +27,7 @@ margin: auto; - border: var(--cru-primary-color) 1px solid; + border: var(--cru-key-container-color) 1px solid; border-radius: 5px; padding: 1.5em; background-color: var(--cru-surface-color); diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx index 79a31954..31dd113b 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.tsx +++ b/FrontEnd/src/views/common/dialog/Dialog.tsx @@ -1,6 +1,9 @@ import { ReactNode } from "react"; import ReactDOM from "react-dom"; import { CSSTransition } from "react-transition-group"; +import classNames from "classnames"; + +import { ThemeColor } from "../common"; import "./Dialog.css"; @@ -11,14 +14,21 @@ if (optionalPortalElement == null) { const portalElement = optionalPortalElement; interface DialogProps { - onClose: () => void; open: boolean; + onClose: () => void; + color?: ThemeColor; children?: ReactNode; disableCloseOnClickOnOverlay?: boolean; } -export default function Dialog(props: DialogProps) { - const { open, onClose, children, disableCloseOnClickOnOverlay } = props; +export default function Dialog({ + open, + onClose, + color, + children, + disableCloseOnClickOnOverlay, +}: DialogProps) { + color = color ?? "primary"; return ReactDOM.createPortal(
-
+
e.stopPropagation()} -- cgit v1.2.3 From c7934c59cb17a4266ea882cdb40be79f22043d10 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Jul 2023 00:31:02 +0800 Subject: ... --- FrontEnd/package.json | 4 - FrontEnd/pnpm-lock.yaml | 75 ----- FrontEnd/src/locales/en/translation.json | 2 +- FrontEnd/src/utilities/base64.ts | 21 +- .../src/views/common/dialog/OperationDialog.tsx | 375 +++++++++------------ FrontEnd/src/views/common/input/InputPanel.tsx | 7 +- 6 files changed, 185 insertions(+), 299 deletions(-) (limited to 'FrontEnd/src/views/common/dialog') diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 268e9636..725b09dd 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -22,19 +22,16 @@ "core-js": "^3.31.1", "i18next": "^23.2.8", "i18next-browser-languagedetector": "^7.1.0", - "js-base64": "^3.7.5", "lodash": "^4.17.21", "marked": "^5.1.1", "moment": "^2.29.4", "react": "^18.2.0", - "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-i18next": "^13.0.1", "react-popper": "^2.3.0", "react-responsive": "^9.0.2", "react-router-dom": "^6.14.1", "react-transition-group": "^4.4.5", - "regenerator-runtime": "^0.13.11", "rxjs": "^7.8.1", "xregexp": "^5.1.1" }, @@ -54,7 +51,6 @@ "@types/react-transition-group": "^4.4.6", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", - "buffer": "^6.0.0", "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/FrontEnd/pnpm-lock.yaml b/FrontEnd/pnpm-lock.yaml index e57ef2f4..a31e5a2c 100644 --- a/FrontEnd/pnpm-lock.yaml +++ b/FrontEnd/pnpm-lock.yaml @@ -35,9 +35,6 @@ dependencies: i18next-browser-languagedetector: specifier: ^7.1.0 version: 7.1.0 - js-base64: - specifier: ^3.7.5 - version: 3.7.5 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -50,9 +47,6 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 - react-color: - specifier: ^2.19.3 - version: 2.19.3(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -71,9 +65,6 @@ dependencies: react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.2.0)(react@18.2.0) - regenerator-runtime: - specifier: ^0.13.11 - version: 0.13.11 rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -127,9 +118,6 @@ devDependencies: '@typescript-eslint/parser': specifier: ^5.61.0 version: 5.61.0(eslint@8.44.0)(typescript@5.1.6) - buffer: - specifier: ^6.0.0 - version: 6.0.3 eslint: specifier: ^8.44.0 version: 8.44.0 @@ -268,14 +256,6 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@icons/material@0.2.4(react@18.2.0): - resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} - peerDependencies: - react: '*' - dependencies: - react: 18.2.0 - dev: false - /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} @@ -1768,10 +1748,6 @@ packages: safe-buffer: 5.2.1 dev: true - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true @@ -1813,13 +1789,6 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.9) dev: true - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -2679,10 +2648,6 @@ packages: '@babel/runtime': 7.22.6 dev: false - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true - /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2856,10 +2821,6 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /js-base64@3.7.5: - resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} - dev: false - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3024,10 +2985,6 @@ packages: p-locate: 5.0.0 dev: true - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false - /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -3065,10 +3022,6 @@ packages: css-mediaquery: 0.1.2 dev: false - /material-colors@1.2.6: - resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} - dev: false - /mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} dev: true @@ -3452,21 +3405,6 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true - /react-color@2.19.3(react@18.2.0): - resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} - peerDependencies: - react: '*' - dependencies: - '@icons/material': 0.2.4(react@18.2.0) - lodash: 4.17.21 - lodash-es: 4.17.21 - material-colors: 1.2.6 - prop-types: 15.8.1 - react: 18.2.0 - reactcss: 1.2.3(react@18.2.0) - tinycolor2: 1.6.0 - dev: false - /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -3584,15 +3522,6 @@ packages: loose-envify: 1.4.0 dev: false - /reactcss@1.2.3(react@18.2.0): - resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} - peerDependencies: - react: '*' - dependencies: - lodash: 4.17.21 - react: 18.2.0 - dev: false - /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -3828,10 +3757,6 @@ packages: resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} dev: true - /tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - dev: false - /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} diff --git a/FrontEnd/src/locales/en/translation.json b/FrontEnd/src/locales/en/translation.json index a73472d2..a7e4efe5 100644 --- a/FrontEnd/src/locales/en/translation.json +++ b/FrontEnd/src/locales/en/translation.json @@ -86,7 +86,7 @@ "ok": "OK!", "processing": "Processing...", "success": "Success!", - "error": "An error occured." + "error": "An error occurred." }, "timeline": { "messageCantSee": "Sorry, you are not allowed to see this timeline.😅", diff --git a/FrontEnd/src/utilities/base64.ts b/FrontEnd/src/utilities/base64.ts index 59de7512..6eece979 100644 --- a/FrontEnd/src/utilities/base64.ts +++ b/FrontEnd/src/utilities/base64.ts @@ -1,8 +1,19 @@ -import { Base64 } from "js-base64"; +function bytesToBase64(bytes: Uint8Array): string { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(""); + return btoa(binString); +} + +export default function base64( + data: Blob | Uint8Array | string, +): Promise { + if (typeof data === "string") { + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + const binString = new TextEncoder().encode(data); + return Promise.resolve(bytesToBase64(binString)); + } -export default function base64(blob: Blob | string): Promise { - if (typeof blob === "string") { - return Promise.resolve(Base64.encode(blob)); + if (data instanceof Uint8Array) { + return Promise.resolve(bytesToBase64(data)); } return new Promise((resolve) => { @@ -10,6 +21,6 @@ export default function base64(blob: Blob | string): Promise { reader.onload = function () { resolve((reader.result as string).replace(/^data:.*;base64,/, "")); }; - reader.readAsDataURL(blob); + reader.readAsDataURL(data); }); } diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx index 71be030a..ad00c424 100644 --- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx @@ -1,13 +1,8 @@ -import { useState } from "react"; -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { TwitterPicker } from "react-color"; +import { useState, ReactNode, ComponentPropsWithoutRef } from "react"; import classNames from "classnames"; import moment from "moment"; -import { convertI18nText, I18nText, UiLogicError } from "@/common"; - -import { PaletteColorType } from "@/palette"; +import { useC, Text, ThemeColor } from "../common"; import Button from "../button/Button"; import LoadingButton from "../button/LoadingButton"; @@ -15,69 +10,61 @@ import Dialog from "./Dialog"; import "./OperationDialog.css"; -interface DefaultErrorPromptProps { - error?: string; +interface DefaultPromptProps { + color?: ThemeColor; + message?: Text; + customMessage?: ReactNode; + className?: string; } -const DefaultErrorPrompt: React.FC = (props) => { - const { t } = useTranslation(); +function DefaultPrompt(props: DefaultPromptProps) { + const { color, message, customMessage, className } = props; - let result =

{t("operationDialog.error")}

; + const c = useC(); - if (props.error != null) { - result = ( - <> - {result} -

{props.error}

- - ); - } - - return result; -}; + return ( +
+

{c(message)}

+ {customMessage} +
+ ); +} export interface OperationDialogTextInput { type: "text"; - label?: I18nText; + label?: Text; password?: boolean; initValue?: string; textFieldProps?: Omit< - React.InputHTMLAttributes, + ComponentPropsWithoutRef<"input">, "type" | "value" | "onChange" >; - helperText?: string; + helperText?: Text; } export interface OperationDialogBoolInput { type: "bool"; - label: I18nText; + label: Text; initValue?: boolean; - helperText?: string; + helperText?: Text; } export interface OperationDialogSelectInputOption { value: string; - label: I18nText; - icon?: React.ReactElement; + label: Text; + icon?: ReactNode; } export interface OperationDialogSelectInput { type: "select"; - label: I18nText; + label: Text; options: OperationDialogSelectInputOption[]; initValue?: string; } -export interface OperationDialogColorInput { - type: "color"; - label?: I18nText; - initValue?: string | null; - canBeNull?: boolean; -} - export interface OperationDialogDateTimeInput { type: "datetime"; - label?: I18nText; + label?: Text; initValue?: string; helperText?: string; } @@ -86,17 +73,18 @@ export type OperationDialogInput = | OperationDialogTextInput | OperationDialogBoolInput | OperationDialogSelectInput - | OperationDialogColorInput | OperationDialogDateTimeInput; interface OperationInputTypeStringToValueTypeMap { text: string; bool: boolean; select: string; - color: string | null; datetime: string; } +type OperationInputValueType = + OperationInputTypeStringToValueTypeMap[keyof OperationInputTypeStringToValueTypeMap]; + type MapOperationInputTypeStringToValueType = Type extends keyof OperationInputTypeStringToValueTypeMap ? OperationInputTypeStringToValueTypeMap[Type] @@ -106,33 +94,15 @@ type MapOperationInputInfoValueType = T extends OperationDialogInput ? MapOperationInputTypeStringToValueType : T; -const initValueMapperMap: { - [T in OperationDialogInput as T["type"]]: ( - item: T - ) => MapOperationInputInfoValueType; -} = { - bool: (item) => item.initValue ?? false, - color: (item) => item.initValue ?? null, - datetime: (item) => { - if (item.initValue != null) { - return moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss"); - } else { - return ""; - } - }, - select: (item) => item.initValue ?? item.options[0].value, - text: (item) => item.initValue ?? "", -}; - type MapOperationInputInfoValueTypeList< - Tuple extends readonly OperationDialogInput[] + Tuple extends readonly OperationDialogInput[], > = { [Index in keyof Tuple]: MapOperationInputInfoValueType; -} & { length: Tuple["length"] }; +}; export type OperationInputError = | { - [index: number]: I18nText | null | undefined; + [index: number]: Text | null | undefined; } | null | undefined; @@ -145,38 +115,68 @@ const isNoError = (error: OperationInputError): boolean => { return true; }; +type ItemValueMapper = { + [T in OperationDialogInput as T["type"]]: ( + item: T, + ) => MapOperationInputInfoValueType; +}; + +type ValueValueMapper = { + [T in OperationDialogInput as T["type"]]: ( + item: MapOperationInputInfoValueType, + ) => MapOperationInputInfoValueType; +}; + +const initValueMapperMap: ItemValueMapper = { + bool: (item) => item.initValue ?? false, + datetime: (item) => + item.initValue != null + ? /* cspell: disable-next-line */ + moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss") + : "", + select: (item) => item.initValue ?? item.options[0].value, + text: (item) => item.initValue ?? "", +}; + +const finalValueMapperMap: ValueValueMapper = { + bool: (value) => value, + datetime: (value) => new Date(value).toISOString(), + select: (value) => value, + text: (value) => value, +}; + export interface OperationDialogProps< TData, - OperationInputInfoList extends readonly OperationDialogInput[] + OperationInputInfoList extends readonly OperationDialogInput[], > { open: boolean; onClose: () => void; - title: I18nText | (() => React.ReactNode); - themeColor?: PaletteColorType; - onProcess: ( - inputs: MapOperationInputInfoValueTypeList - ) => Promise; + + themeColor?: ThemeColor; + title: Text; + inputPrompt?: Text; + processPrompt?: Text; + successPrompt?: (data: TData) => ReactNode; + failurePrompt?: (error: unknown) => ReactNode; + inputScheme?: OperationInputInfoList; inputValidator?: ( - inputs: MapOperationInputInfoValueTypeList + inputs: MapOperationInputInfoValueTypeList, ) => OperationInputError; - inputPrompt?: I18nText | (() => React.ReactNode); - processPrompt?: () => React.ReactNode; - successPrompt?: (data: TData) => React.ReactNode; - failurePrompt?: (error: unknown) => React.ReactNode; + + onProcess: ( + inputs: MapOperationInputInfoValueTypeList, + ) => Promise; onSuccessAndClose?: (data: TData) => void; } -const OperationDialog = < +function OperationDialog< TData, - OperationInputInfoList extends readonly OperationDialogInput[] ->( - props: OperationDialogProps -): React.ReactElement => { - const inputScheme = (props.inputScheme ?? - []) as readonly OperationDialogInput[]; + OperationInputInfoList extends readonly OperationDialogInput[], +>(props: OperationDialogProps) { + const inputScheme = props.inputScheme ?? ([] as const); - const { t } = useTranslation(); + const c = useC(); type Step = | { type: "input" } @@ -189,48 +189,42 @@ const OperationDialog = < type: "failure"; data: unknown; }; + const [step, setStep] = useState({ type: "input" }); - type ValueType = boolean | string | null | undefined; - - const [values, setValues] = useState( - inputScheme.map((item) => { - if (item.type in initValueMapperMap) { - return ( - initValueMapperMap[item.type] as ( - i: OperationDialogInput - ) => ValueType - )(item); - } else { - throw new UiLogicError("Unknown input scheme."); - } - }) + type Values = MapOperationInputInfoValueTypeList; + + const [values, setValues] = useState( + () => + inputScheme.map((item) => + initValueMapperMap[item.type](item as never), + ) as Values, ); + const [dirtyList, setDirtyList] = useState(() => - inputScheme.map(() => false) + inputScheme.map(() => false), ); + const [inputError, setInputError] = useState(); - const close = (): void => { + function close() { if (step.type !== "process") { props.onClose(); if (step.type === "success" && props.onSuccessAndClose) { props.onSuccessAndClose(step.data); } } else { - console.log("Attempt to close modal when processing."); + console.log("Attempt to close modal dialog when processing."); } - }; + } - const onConfirm = (): void => { + function onConfirm() { setStep({ type: "process" }); props .onProcess( - values.map((v, index) => { - if (inputScheme[index].type === "datetime" && v !== "") - return new Date(v as string).toISOString(); - else return v; - }) as unknown as MapOperationInputInfoValueTypeList + values.map((value, index) => + finalValueMapperMap[inputScheme[index].type](value as never), + ) as Values, ) .then( (d) => { @@ -244,56 +238,51 @@ const OperationDialog = < type: "failure", data: e, }); - } + }, ); - }; + } - let body: React.ReactNode; + let body: ReactNode; if (step.type === "input" || step.type === "process") { const process = step.type === "process"; - let inputPrompt = - typeof props.inputPrompt === "function" - ? props.inputPrompt() - : convertI18nText(props.inputPrompt, t); - inputPrompt =
{inputPrompt}
; - - const validate = (values: ValueType[]): boolean => { + const validate = (values: Values): boolean => { const { inputValidator } = props; if (inputValidator != null) { - const result = inputValidator( - values as unknown as MapOperationInputInfoValueTypeList - ); + const result = inputValidator(values); setInputError(result); return isNoError(result); } return true; }; - const updateValue = (index: number, newValue: ValueType): void => { + const updateValue = ( + index: number, + newValue: OperationInputValueType, + ): void => { const oldValues = values; const newValues = oldValues.slice(); newValues[index] = newValue; - setValues(newValues); + setValues(newValues as Values); if (dirtyList[index] === false) { const newDirtyList = dirtyList.slice(); newDirtyList[index] = true; setDirtyList(newDirtyList); } - validate(newValues); + validate(newValues as Values); }; const canProcess = isNoError(inputError); body = ( - <> +
- {inputPrompt} - {inputScheme.map((item, index) => { +
{c(props.inputPrompt)}
+ {inputScheme.map((item: OperationDialogInput, index: number) => { const value = values[index]; const error: string | null = dirtyList[index] && inputError != null - ? convertI18nText(inputError[index], t) + ? c(inputError[index]) : null; if (item.type === "text") { @@ -302,31 +291,31 @@ const OperationDialog = < key={index} className={classNames( "cru-operation-dialog-group", - error != null ? "error" : null + error && "error", )} > {item.label && ( )} { - const v = e.target.value; + onChange={(event) => { + const v = event.target.value; updateValue(index, v); }} disabled={process} /> - {error != null && ( + {error && (
{error}
)} {item.helperText && (
- {t(item.helperText)} + {c(item.helperText)}
)}
@@ -337,28 +326,29 @@ const OperationDialog = < key={index} className={classNames( "cru-operation-dialog-group", - error != null ? "error" : null + error && "error", )} > { - updateValue(index, event.currentTarget.checked); + const v = event.currentTarget.checked; + updateValue(index, v); }} disabled={process} /> - {error != null && ( + {error && (
{error}
)} {item.helperText && (
- {t(item.helperText)} + {c(item.helperText)}
)}
@@ -369,16 +359,17 @@ const OperationDialog = < key={index} className={classNames( "cru-operation-dialog-group", - error != null ? "error" : null + error && "error", )} >
); - } else if (item.type === "color") { - return ( -
- {item.canBeNull ? ( - { - if (event.currentTarget.checked) { - updateValue(index, "#007bff"); - } else { - updateValue(index, null); - } - }} - disabled={process} - /> - ) : null} - - {value !== null && ( - updateValue(index, result.hex)} - /> - )} -
- ); } else if (item.type === "datetime") { return (
{item.label && ( )} { - const v = e.target.value; + onChange={(event) => { + const v = event.target.value; updateValue(index, v); }} disabled={process} /> - {error != null &&
{error}
} + {error && ( +
+ {error} +
+ )}
); } @@ -477,55 +437,50 @@ const OperationDialog = < } }} > - {t("operationDialog.confirm")} + {c("operationDialog.confirm")}
- +
); } else { - let content: React.ReactNode; const result = step; - if (result.type === "success") { - content = - props.successPrompt?.(result.data) ?? t("operationDialog.success"); - if (typeof content === "string") - content =

{content}

; - } else { - content = props.failurePrompt?.(result.data) ?? ; - if (typeof content === "string") - content = ; - } + + const promptProps: DefaultPromptProps = + result.type === "success" + ? { + color: "success", + message: "operationDialog.success", + customMessage: props.successPrompt?.(result.data), + } + : { + color: "danger", + message: "operationDialog.error", + customMessage: props.failurePrompt?.(result.data), + }; body = ( - <> -
{content}
+
+
- +
); } - const title = - typeof props.title === "function" - ? props.title() - : convertI18nText(props.title, t); - return ( -

- {title} -

+ {c(props.title)} +

{body} ); -}; +} export default OperationDialog; diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx index 234ed267..27937a05 100644 --- a/FrontEnd/src/views/common/input/InputPanel.tsx +++ b/FrontEnd/src/views/common/input/InputPanel.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; -import { TwitterPicker } from "react-color"; import { convertI18nText, I18nText } from "@/common"; @@ -89,14 +88,14 @@ export interface InputPanelProps { values: MapInputListToValueTypeList; onChange: ( values: MapInputListToValueTypeList, - index: number + index: number, ) => void; error?: InputPanelError; disable?: boolean; } const InputPanel = ( - props: InputPanelProps + props: InputPanelProps, ): React.ReactElement => { const { values, onChange, scheme, error, disable } = props; @@ -108,7 +107,7 @@ const InputPanel = ( newValues[index] = newValue; onChange( newValues as unknown as MapInputListToValueTypeList, - index + index, ); }; -- cgit v1.2.3 From 4f8d933994c576dc180fae23a3dca477d2354939 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Jul 2023 21:48:48 +0800 Subject: ... --- .../src/pages/setting/ChangePasswordDialog.tsx | 11 +- .../src/views/common/dialog/OperationDialog.css | 17 +- .../src/views/common/dialog/OperationDialog.tsx | 409 ++++----------------- FrontEnd/src/views/common/input/InputGroup.css | 25 ++ FrontEnd/src/views/common/input/InputGroup.tsx | 362 ++++++++++++++++++ FrontEnd/src/views/common/input/InputPanel.css | 25 -- FrontEnd/src/views/common/input/InputPanel.tsx | 246 ------------- FrontEnd/src/views/register/index.tsx | 9 +- 8 files changed, 483 insertions(+), 621 deletions(-) create mode 100644 FrontEnd/src/views/common/input/InputGroup.css create mode 100644 FrontEnd/src/views/common/input/InputGroup.tsx delete mode 100644 FrontEnd/src/views/common/input/InputPanel.css delete mode 100644 FrontEnd/src/views/common/input/InputPanel.tsx (limited to 'FrontEnd/src/views/common/dialog') diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index a523b454..5505137e 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -1,24 +1,25 @@ import { useState } from "react"; -import * as React from "react"; import { useNavigate } from "react-router-dom"; import { userService } from "@/services/user"; import OperationDialog from "@/views/common/dialog/OperationDialog"; -export interface ChangePasswordDialogProps { +interface ChangePasswordDialogProps { open: boolean; close: () => void; } -const ChangePasswordDialog: React.FC = (props) => { +export function ChangePasswordDialog(props: ChangePasswordDialogProps) { + const { open, close } = props; + const navigate = useNavigate(); const [redirect, setRedirect] = useState(false); return ( = (props) => { }} /> ); -}; +} export default ChangePasswordDialog; diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css index 2f7617d0..19c5d806 100644 --- a/FrontEnd/src/views/common/dialog/OperationDialog.css +++ b/FrontEnd/src/views/common/dialog/OperationDialog.css @@ -1,3 +1,18 @@ +.cru-operation-dialog-title { + font-size: 1.2em; + font-weight: bold; + color: var(--cru-key-color); + margin-bottom: 0.5em; +} + +.cru-operation-dialog-prompt { + color: var(--cru-surface-on-color); +} + +.cru-operation-dialog-main-area { + margin-top: 0.5em; +} + .cru-operation-dialog-group { display: block; margin: 0.4em 0; @@ -22,4 +37,4 @@ display: block; font-size: 0.8em; color: var(--cru-primary-color); -} +} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx index ad00c424..ad9bf5c1 100644 --- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx @@ -1,180 +1,76 @@ -import { useState, ReactNode, ComponentPropsWithoutRef } from "react"; +import { useState, ReactNode } from "react"; import classNames from "classnames"; -import moment from "moment"; import { useC, Text, ThemeColor } from "../common"; import Button from "../button/Button"; +import { + default as InputGroup, + InputErrors, + InputList, + Validator, + Values, + useDirties, +} from "../input/InputGroup"; import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; import "./OperationDialog.css"; -interface DefaultPromptProps { - color?: ThemeColor; +interface OperationDialogPromptProps { message?: Text; customMessage?: ReactNode; className?: string; } -function DefaultPrompt(props: DefaultPromptProps) { - const { color, message, customMessage, className } = props; +function OperationDialogPrompt(props: OperationDialogPromptProps) { + const { message, customMessage, className } = props; const c = useC(); return ( -
-

{c(message)}

+
+ {message &&

{c(message)}

} {customMessage}
); } -export interface OperationDialogTextInput { - type: "text"; - label?: Text; - password?: boolean; - initValue?: string; - textFieldProps?: Omit< - ComponentPropsWithoutRef<"input">, - "type" | "value" | "onChange" - >; - helperText?: Text; -} - -export interface OperationDialogBoolInput { - type: "bool"; - label: Text; - initValue?: boolean; - helperText?: Text; -} - -export interface OperationDialogSelectInputOption { - value: string; - label: Text; - icon?: ReactNode; -} - -export interface OperationDialogSelectInput { - type: "select"; - label: Text; - options: OperationDialogSelectInputOption[]; - initValue?: string; -} - -export interface OperationDialogDateTimeInput { - type: "datetime"; - label?: Text; - initValue?: string; - helperText?: string; -} - -export type OperationDialogInput = - | OperationDialogTextInput - | OperationDialogBoolInput - | OperationDialogSelectInput - | OperationDialogDateTimeInput; - -interface OperationInputTypeStringToValueTypeMap { - text: string; - bool: boolean; - select: string; - datetime: string; -} - -type OperationInputValueType = - OperationInputTypeStringToValueTypeMap[keyof OperationInputTypeStringToValueTypeMap]; - -type MapOperationInputTypeStringToValueType = - Type extends keyof OperationInputTypeStringToValueTypeMap - ? OperationInputTypeStringToValueTypeMap[Type] - : never; - -type MapOperationInputInfoValueType = T extends OperationDialogInput - ? MapOperationInputTypeStringToValueType - : T; - -type MapOperationInputInfoValueTypeList< - Tuple extends readonly OperationDialogInput[], -> = { - [Index in keyof Tuple]: MapOperationInputInfoValueType; -}; - -export type OperationInputError = - | { - [index: number]: Text | null | undefined; - } - | null - | undefined; - -const isNoError = (error: OperationInputError): boolean => { - if (error == null) return true; - for (const key in error) { - if (error[key] != null) return false; - } - return true; -}; - -type ItemValueMapper = { - [T in OperationDialogInput as T["type"]]: ( - item: T, - ) => MapOperationInputInfoValueType; -}; - -type ValueValueMapper = { - [T in OperationDialogInput as T["type"]]: ( - item: MapOperationInputInfoValueType, - ) => MapOperationInputInfoValueType; -}; - -const initValueMapperMap: ItemValueMapper = { - bool: (item) => item.initValue ?? false, - datetime: (item) => - item.initValue != null - ? /* cspell: disable-next-line */ - moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss") - : "", - select: (item) => item.initValue ?? item.options[0].value, - text: (item) => item.initValue ?? "", -}; - -const finalValueMapperMap: ValueValueMapper = { - bool: (value) => value, - datetime: (value) => new Date(value).toISOString(), - select: (value) => value, - text: (value) => value, -}; - -export interface OperationDialogProps< - TData, - OperationInputInfoList extends readonly OperationDialogInput[], -> { +export interface OperationDialogProps { open: boolean; onClose: () => void; - themeColor?: ThemeColor; + color?: ThemeColor; title: Text; inputPrompt?: Text; processPrompt?: Text; successPrompt?: (data: TData) => ReactNode; failurePrompt?: (error: unknown) => ReactNode; - inputScheme?: OperationInputInfoList; - inputValidator?: ( - inputs: MapOperationInputInfoValueTypeList, - ) => OperationInputError; + inputs: Inputs; + validator?: Validator; - onProcess: ( - inputs: MapOperationInputInfoValueTypeList, - ) => Promise; + onProcess: (inputs: Values) => Promise; onSuccessAndClose?: (data: TData) => void; } -function OperationDialog< - TData, - OperationInputInfoList extends readonly OperationDialogInput[], ->(props: OperationDialogProps) { - const inputScheme = props.inputScheme ?? ([] as const); +function OperationDialog( + props: OperationDialogProps, +) { + const { + open, + onClose, + color, + title, + inputPrompt, + processPrompt, + successPrompt, + failurePrompt, + inputs, + validator, + onProcess, + onSuccessAndClose, + } = props; const c = useC(); @@ -191,21 +87,9 @@ function OperationDialog< }; const [step, setStep] = useState({ type: "input" }); - - type Values = MapOperationInputInfoValueTypeList; - - const [values, setValues] = useState( - () => - inputScheme.map((item) => - initValueMapperMap[item.type](item as never), - ) as Values, - ); - - const [dirtyList, setDirtyList] = useState(() => - inputScheme.map(() => false), - ); - - const [inputError, setInputError] = useState(); + const [values, setValues] = useState>(); + const [errors, setErrors] = useState(); + const [dirties, setDirties, dirtyAll] = useDirties(); function close() { if (step.type !== "process") { @@ -244,178 +128,28 @@ function OperationDialog< let body: ReactNode; if (step.type === "input" || step.type === "process") { - const process = step.type === "process"; - - const validate = (values: Values): boolean => { - const { inputValidator } = props; - if (inputValidator != null) { - const result = inputValidator(values); - setInputError(result); - return isNoError(result); - } - return true; - }; - - const updateValue = ( - index: number, - newValue: OperationInputValueType, - ): void => { - const oldValues = values; - const newValues = oldValues.slice(); - newValues[index] = newValue; - setValues(newValues as Values); - if (dirtyList[index] === false) { - const newDirtyList = dirtyList.slice(); - newDirtyList[index] = true; - setDirtyList(newDirtyList); - } - validate(newValues as Values); - }; - - const canProcess = isNoError(inputError); + const isProcessing = step.type === "process"; + const hasError = errors.length > 0; body = (
-
{c(props.inputPrompt)}
- {inputScheme.map((item: OperationDialogInput, index: number) => { - const value = values[index]; - const error: string | null = - dirtyList[index] && inputError != null - ? c(inputError[index]) - : null; - - if (item.type === "text") { - return ( -
- {item.label && ( - - )} - { - const v = event.target.value; - updateValue(index, v); - }} - disabled={process} - /> - {error && ( -
- {error} -
- )} - {item.helperText && ( -
- {c(item.helperText)} -
- )} -
- ); - } else if (item.type === "bool") { - return ( -
- { - const v = event.currentTarget.checked; - updateValue(index, v); - }} - disabled={process} - /> - - {error && ( -
- {error} -
- )} - {item.helperText && ( -
- {c(item.helperText)} -
- )} -
- ); - } else if (item.type === "select") { - return ( -
- - -
- ); - } else if (item.type === "datetime") { - return ( -
- {item.label && ( - - )} - { - const v = event.target.value; - updateValue(index, v); - }} - disabled={process} - /> - {error && ( -
- {error} -
- )} -
- ); - } - })} + + { + setValues(values); + setErrors(errors); + }} + dirties={dirties} + onDirty={setDirties} + />

@@ -424,14 +158,14 @@ function OperationDialog< color="secondary" outline onClick={close} - disabled={process} + disabled={isProcessing} /> { - setDirtyList(inputScheme.map(() => true)); + dirtyAll(); if (validate(values)) { onConfirm(); } @@ -445,21 +179,19 @@ function OperationDialog< } else { const result = step; - const promptProps: DefaultPromptProps = + const promptProps: OperationDialogPromptProps = result.type === "success" ? { - color: "success", message: "operationDialog.success", customMessage: props.successPrompt?.(result.data), } : { - color: "danger", message: "operationDialog.error", customMessage: props.failurePrompt?.(result.data), }; body = (
- +
); } return ( - +
-
{c(props.title)}
+
{c(title)}

{body}
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 7c33def7..232edfc9 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -23,7 +23,7 @@ * `useInputs` hook takes care of logic and generate props for `InputGroup`. */ -import { useState, useRef, Ref } from "react"; +import { useState, Ref } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; @@ -86,7 +86,7 @@ export type InputScheme = { validator?: Validator; }; -export type InputState = { +export type InputData = { values: InputValueDict; errors: InputErrorDict; disabled: InputDisabledDict; @@ -95,16 +95,18 @@ export type InputState = { export type State = { scheme: InputScheme; - state: InputState; + data: InputData; }; -export type StateInitializer = Partial; +export type DataInitializeInfo = Partial; -export type Initializer = { +export type InitializeInfo = { scheme: InputScheme; - stateInit?: Partial; + dataInit?: DataInitializeInfo; }; +export type Initialize + export interface InputGroupProps { color?: ThemeColor; containerClassName?: string; @@ -114,7 +116,7 @@ export interface InputGroupProps { onChange: (index: number, value: Input["value"]) => void; } -function cleanObject>(o: O): O { +function cleanObject(o: Record): Record { const result = { ...o }; for (const key of Object.keys(result)) { if (result[key] == null) { @@ -124,8 +126,23 @@ function cleanObject>(o: O): O { return result; } -export function useInputs(options: { init?: () => Initializer }): { +export type ConfirmResult = + | { + type: "ok"; + values: InputValueDict; + } + | { + type: "error"; + errors: InputErrorDict; + }; + +export function useInputs(options: { + init: InitializeInfo | (() => InitializeInfo); +}): { inputGroupProps: InputGroupProps; + hasError: boolean; + confirm: () => ConfirmResult; + setAllDisabled: (disabled: boolean) => void; } { function initializeValue( input: InputInfo, @@ -141,54 +158,59 @@ export function useInputs(options: { init?: () => Initializer }): { throw new Error("Unknown input type"); } - function initialize(initializer: Initializer): State { - const { scheme, stateInit } = initializer; + function initialize(info: InitializeInfo): State { + const { scheme, dataInit } = info; const { inputs, validator } = scheme; const keys = inputs.map((input) => input.key); if (process.env.NODE_ENV === "development") { - const checkKeys = (dict: Record) => { - for (const key of Object.keys(dict)) { - if (!keys.includes(key)) { - console.warn(""); + const checkKeys = (dict: Record | undefined) => { + if (dict != null) { + for (const key of Object.keys(dict)) { + if (!keys.includes(key)) { + console.warn(""); + } } } }; - checkKeys(stateInit?.values ?? {}); - checkKeys(stateInit?.errors ?? {}); - checkKeys(stateInit?.disabled ?? {}); - checkKeys(stateInit?.dirties ?? {}); + checkKeys(dataInit?.values); + checkKeys(dataInit?.errors); + checkKeys(dataInit?.disabled); + checkKeys(dataInit?.dirties); + } + + function clean(dict: Record | undefined): Record { + return dict != null ? cleanObject(dict) : {}; } const values: InputValueDict = {}; - let errors: InputErrorDict = cleanObject( - initializer.stateInit?.errors ?? {}, - ); - const disabled: InputDisabledDict = cleanObject( - initializer.stateInit?.disabled ?? {}, - ); - const dirties: InputDirtyDict = cleanObject( - initializer.stateInit?.dirties ?? {}, - ); + const disabled: InputDisabledDict = clean(info.dataInit?.disabled); + const dirties: InputDirtyDict = clean(info.dataInit?.dirties); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; const { key } = input; - values[key] = initializeValue(input, stateInit?.values?.[key]); - if (!(key in dirties)) { - dirties[key] = false; - } + values[key] = initializeValue(input, dataInit?.values?.[key]); } - if (Object.keys(errors).length === 0 && validator != null) { - errors = validator(values, inputs); + let errors = info.dataInit?.errors; + + if (errors != null) { + if (process.env.NODE_ENV === "development") { + console.log( + "You explicitly set errors (not undefined) in initializer, so validator won't run.", + ); + } + errors = cleanObject(errors); + } else { + errors = validator?.(values, inputs) ?? {}; } return { scheme, - state: { + data: { values, errors, disabled, @@ -198,31 +220,95 @@ export function useInputs(options: { init?: () => Initializer }): { } const { init } = options; + const initializer = typeof init === "function" ? init : () => init; + + const [state, setState] = useState(() => initialize(initializer())); + + const { scheme, data } = state; + const { validator } = scheme; + + function createAllBooleanDict(value: boolean): Record { + const result: InputDirtyDict = {}; + for (const key of scheme.inputs.map((input) => input.key)) { + result[key] = value; + } + return result; + } + + const createAllDirties = () => createAllBooleanDict(true); const componentInputs: Input[] = []; - for (let i = 0; i < inputs.length; i++) { - const input = { ...inputs[i] }; - const error = dirties[i] - ? errors.find((e) => e.index === i)?.message - : undefined; - const componentInput: ExtendInputForComponent = { + for (let i = 0; i < scheme.inputs.length; i++) { + const input = scheme.inputs[i]; + const value = data.values[input.key]; + const error = data.errors[input.key]; + const disabled = data.disabled[input.key] ?? false; + const dirty = data.dirties[input.key] ?? false; + const componentInput: Input = { ...input, - value: values[i], + value: value as never, disabled, - error, + error: dirty ? error : undefined, }; componentInputs.push(componentInput); } - const dirtyAll = () => { - if (dirties != null) { - setDirties(new Array(dirties.length).fill(true) as Dirties); - } - }; - return { - inputGroupProps: {}, + inputGroupProps: { + inputs: componentInputs, + onChange: (index, value) => { + const input = scheme.inputs[index]; + const { key } = input; + const newValues = { ...data.values, [key]: value }; + const newDirties = { ...data.dirties, [key]: true }; + const newErrors = validator?.(newValues, scheme.inputs) ?? {}; + setState({ + scheme, + data: { + ...data, + values: newValues, + errors: newErrors, + dirties: newDirties, + }, + }); + }, + }, + hasError: Object.keys(data.errors).length > 0, + confirm() { + const newDirties = createAllDirties(); + const newErrors = validator?.(data.values, scheme.inputs) ?? {}; + + setState({ + scheme, + data: { + ...data, + dirties: newDirties, + errors: newErrors, + }, + }); + + if (Object.keys(newErrors).length === 0) { + return { + type: "error", + errors: newErrors, + }; + } else { + return { + type: "ok", + values: data.values, + }; + } + }, + setAllDisabled(disabled: boolean) { + setState({ + scheme, + data: { + ...data, + disabled: createAllBooleanDict(disabled), + }, + }); + }, }; } diff --git a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx index fc55185c..76f542c1 100644 --- a/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/PostPropertyChangeDialog.tsx @@ -15,7 +15,7 @@ function PostPropertyChangeDialog(props: { return ( = (props) => { return ( { diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx index bd5bef4c..a0eebdbb 100644 --- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx @@ -55,7 +55,7 @@ const TimelinePropertyChangeDialog: React.FC< ] as const } open={props.open} - onClose={props.close} + close={props.close} onProcess={([newTitle, newVisibility, newDescription, newColor]) => { const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { -- cgit v1.2.3 From 2d4a75a21a8a97db8017b56e321c56c7d70bc674 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 29 Jul 2023 01:22:38 +0800 Subject: ... --- FrontEnd/src/index.css | 16 ----- .../src/pages/setting/ChangeNicknameDialog.tsx | 29 ++++---- .../src/pages/setting/ChangePasswordDialog.tsx | 79 +++++++++++++--------- FrontEnd/src/views/common/button/Button.css | 1 + FrontEnd/src/views/common/dialog/Dialog.css | 11 ++- FrontEnd/src/views/common/dialog/Dialog.tsx | 27 +++----- .../src/views/common/dialog/OperationDialog.css | 29 ++------ .../src/views/common/dialog/OperationDialog.tsx | 32 ++++----- FrontEnd/src/views/common/input/InputGroup.css | 43 ++++++++++-- FrontEnd/src/views/common/input/InputGroup.tsx | 44 ++++++------ FrontEnd/src/views/common/theme.css | 2 + 11 files changed, 162 insertions(+), 151 deletions(-) (limited to 'FrontEnd/src/views/common/dialog') diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index 49791c23..ee92520b 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -30,22 +30,6 @@ textarea:focus { border-color: var(--cru-primary-color); } -input:not([type="checkbox"]):not([type="radio"]) { - resize: none; - outline: none; - border: 1px solid; - transition: all 0.5s; - border-color: var(--cru-background-2-color); -} - -input:hover:not([type="checkbox"]):not([type="radio"]) { - border-color: var(--cru-primary-r2-color); -} - -input:focus:not([type="checkbox"]):not([type="radio"]) { - border-color: var(--cru-primary-color); -} - .white-space-no-wrap { white-space: nowrap; } diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 58bbac5f..5606ce94 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -1,6 +1,5 @@ import { getHttpUserClient } from "@/http/user"; -import { useUser } from "@/services/user"; -import * as React from "react"; +import { useUserLoggedIn } from "@/services/user"; import OperationDialog from "@/views/common/dialog/OperationDialog"; @@ -9,26 +8,28 @@ export interface ChangeNicknameDialogProps { close: () => void; } -const ChangeNicknameDialog: React.FC = (props) => { - const user = useUser(); +export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { + const { open, close } = props; - if (user == null) return null; + const user = useUserLoggedIn(); return ( { + onProcess={({ newNickname }) => { return getHttpUserClient().patch(user.username, { - nickname: newNickname, + nickname: newNickname as string, }); }} - close={props.close} + close={close} /> ); -}; - -export default ChangeNicknameDialog; +} diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index 9ca95168..407f3051 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -3,7 +3,9 @@ import { useNavigate } from "react-router-dom"; import { userService } from "@/services/user"; -import OperationDialog from "@/views/common/dialog/OperationDialog"; +import OperationDialog, { + InputErrorDict, +} from "@/views/common/dialog/OperationDialog"; interface ChangePasswordDialogProps { open: boolean; @@ -20,45 +22,56 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { return ( { + const result: InputErrorDict = {}; + if (oldPassword === "") { + result["oldPassword"] = + "settings.dialogChangePassword.errorEmptyOldPassword"; + } + if (newPassword === "") { + result["newPassword"] = + "settings.dialogChangePassword.errorEmptyNewPassword"; + } + if (retypedNewPassword !== newPassword) { + result["retypedNewPassword"] = + "settings.dialogChangePassword.errorRetypeNotMatch"; + } + return result; }, - { - type: "text", - label: "settings.dialogChangePassword.inputNewPassword", - password: true, - }, - { - type: "text", - label: "settings.dialogChangePassword.inputRetypeNewPassword", - password: true, - }, - ]} - inputValidator={([oldPassword, newPassword, retypedNewPassword]) => { - const result: Record = {}; - if (oldPassword === "") { - result[0] = "settings.dialogChangePassword.errorEmptyOldPassword"; - } - if (newPassword === "") { - result[1] = "settings.dialogChangePassword.errorEmptyNewPassword"; - } - if (retypedNewPassword !== newPassword) { - result[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; - } - return result; }} - onProcess={async ([oldPassword, newPassword]) => { - await userService.changePassword(oldPassword, newPassword); + onProcess={async ({ oldPassword, newPassword }) => { + await userService.changePassword( + oldPassword as string, + newPassword as string, + ); setRedirect(true); }} - close={() => { - props.close(); + onSuccessAndClose={() => { if (redirect) { navigate("/login"); } diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css index 12c6903e..fe619f9d 100644 --- a/FrontEnd/src/views/common/button/Button.css +++ b/FrontEnd/src/views/common/button/Button.css @@ -5,6 +5,7 @@ border-radius: 0.2em; border: 1px solid; cursor: pointer; + background-color: var(--cru-surface-color); } .cru-button:not(.outline) { diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css index 99e1a516..8f12614b 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.css +++ b/FrontEnd/src/views/common/dialog/Dialog.css @@ -6,7 +6,6 @@ right: 0; bottom: 0; display: flex; - padding: 2em; overflow: auto; } @@ -14,8 +13,8 @@ position: absolute; z-index: -1; left: 0; - top: 0; right: 0; + top: 0; bottom: 0; background-color: var(--cru-surface-dim-color); opacity: 0.8; @@ -25,7 +24,7 @@ max-width: 100%; min-width: 30vw; - margin: auto; + margin: 2em auto; border: var(--cru-key-container-color) 1px solid; border-radius: 5px; @@ -33,6 +32,12 @@ background-color: var(--cru-surface-color); } +@media (min-width: 576px) { + .cru-dialog-container { + max-width: 800px; + } +} + .cru-dialog-bottom-area { display: flex; justify-content: flex-end; diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx index 31dd113b..9ce344dc 100644 --- a/FrontEnd/src/views/common/dialog/Dialog.tsx +++ b/FrontEnd/src/views/common/dialog/Dialog.tsx @@ -38,23 +38,18 @@ export default function Dialog({ timeout={300} classNames="cru-dialog" > -
{ - onClose(); - } - } - > -
+
e.stopPropagation()} - > - {children} -
+ className="cru-dialog-background" + onClick={ + disableCloseOnClickOnOverlay + ? undefined + : () => { + onClose(); + } + } + /> +
{children}
, portalElement, diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css index 19c5d806..43cdb692 100644 --- a/FrontEnd/src/views/common/dialog/OperationDialog.css +++ b/FrontEnd/src/views/common/dialog/OperationDialog.css @@ -9,32 +9,15 @@ color: var(--cru-surface-on-color); } -.cru-operation-dialog-main-area { - margin-top: 0.5em; -} - -.cru-operation-dialog-group { - display: block; - margin: 0.4em 0; -} - -.cru-operation-dialog-label { - display: block; - color: var(--cru-primary-color); +.cru-dialog-middle-area { + margin: 0.5em 0; } -.cru-operation-dialog-inline-label { - margin-inline-start: 0.5em; +.cru-dialog-bottom-area { + margin-top: 0.5em; } -.cru-operation-dialog-error-text { +.cru-operation-dialog-input-group { display: block; - font-size: 0.8em; - color: var(--cru-danger-color); + margin: 0.5em 0; } - -.cru-operation-dialog-helper-text { - display: block; - font-size: 0.8em; - color: var(--cru-primary-color); -} \ No newline at end of file diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx index 97d135e9..8aab45d9 100644 --- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx @@ -7,15 +7,17 @@ import Button from "../button/Button"; import { useInputs, InputGroup, - InitializeInfo as InputInitializer, + Initializer as InputInitializer, InputValueDict, - InputScheme, + InputErrorDict, } from "../input/InputGroup"; import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; import "./OperationDialog.css"; +export type { InputInitializer, InputValueDict, InputErrorDict }; + interface OperationDialogPromptProps { message?: Text; customMessage?: ReactNode; @@ -40,13 +42,13 @@ export interface OperationDialogProps { close: () => void; color?: ThemeColor; + inputColor?: ThemeColor; title: Text; inputPrompt?: Text; successPrompt?: (data: TData) => ReactNode; failurePrompt?: (error: unknown) => ReactNode; - inputInit?: InputInitializer; - inputScheme?: InputScheme; + inputs: InputInitializer; onProcess: (inputs: InputValueDict) => Promise; onSuccessAndClose?: (data: TData) => void; @@ -57,25 +59,16 @@ function OperationDialog(props: OperationDialogProps) { open, close, color, + inputColor, title, inputPrompt, successPrompt, failurePrompt, - inputInit, - inputScheme, + inputs, onProcess, onSuccessAndClose, } = props; - if (process.env.NODE_ENV === "development") { - if (inputScheme == null && inputInit == null) { - throw Error("Scheme or Init? Choose one and create one."); - } - if (inputScheme != null && inputInit != null) { - throw Error("Scheme or Init? Choose one and drop one"); - } - } - const c = useC(); type Step = @@ -93,14 +86,13 @@ function OperationDialog(props: OperationDialogProps) { const [step, setStep] = useState({ type: "input" }); const { inputGroupProps, hasError, setAllDisabled, confirm } = useInputs({ - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - init: inputInit ?? { scheme: inputScheme! }, + init: inputs, }); function onClose() { if (step.type !== "process") { close(); - if (step.type === "success" && props.onSuccessAndClose) { + if (step.type === "success" && onSuccessAndClose) { onSuccessAndClose?.(step.data); } } else { @@ -136,11 +128,11 @@ function OperationDialog(props: OperationDialogProps) { body = (
-
+
diff --git a/FrontEnd/src/views/common/input/InputGroup.css b/FrontEnd/src/views/common/input/InputGroup.css index f9d6ac8b..1763ea53 100644 --- a/FrontEnd/src/views/common/input/InputGroup.css +++ b/FrontEnd/src/views/common/input/InputGroup.css @@ -1,25 +1,54 @@ -.cru-input-panel-group { +.cru-input-group { display: block; +} + +.cru-input-container { margin: 0.4em 0; } -.cru-input-panel-label { +.cru-input-label { display: block; - color: var(--cru-primary-color); + color: var(--cru-key-color); + font-size: 0.9em; + margin-bottom: 0.3em; } -.cru-input-panel-inline-label { +.cru-input-label-inline { margin-inline-start: 0.5em; } -.cru-input-panel-error-text { +.cru-input-type-text input { + appearance: none; + display: block; + border: 1px solid var(--cru-surface-outline-color); + color: var(--cru-surface-on-color); + background-color: var(--cru-surface-color); + margin: 0; + padding: 0; + font-size: 1.2em; +} + +.cru-input-type-text input:hover { + border-color: var(--cru-key-color); +} + +.cru-input-type-text input:focus { + border-color: var(--cru-key-color); +} + +.cru-input-type-text input:disabled { + border-color: var(--cru-surface-on-color); +} + +.cru-input-error { display: block; font-size: 0.8em; color: var(--cru-danger-color); + margin-top: 0.4em; } -.cru-input-panel-helper-text { +.cru-input-helper { display: block; font-size: 0.8em; color: var(--cru-primary-color); -} +} \ No newline at end of file diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 232edfc9..eed8266b 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -98,14 +98,16 @@ export type State = { data: InputData; }; -export type DataInitializeInfo = Partial; +export type DataInitialization = Partial; -export type InitializeInfo = { +export type Initialization = { scheme: InputScheme; - dataInit?: DataInitializeInfo; + dataInit?: DataInitialization; }; -export type Initialize +export type GeneralInitialization = Initialization | InputScheme | InputInfo[]; + +export type Initializer = GeneralInitialization | (() => GeneralInitialization); export interface InputGroupProps { color?: ThemeColor; @@ -136,9 +138,7 @@ export type ConfirmResult = errors: InputErrorDict; }; -export function useInputs(options: { - init: InitializeInfo | (() => InitializeInfo); -}): { +export function useInputs(options: { init: Initializer }): { inputGroupProps: InputGroupProps; hasError: boolean; confirm: () => ConfirmResult; @@ -158,8 +158,14 @@ export function useInputs(options: { throw new Error("Unknown input type"); } - function initialize(info: InitializeInfo): State { - const { scheme, dataInit } = info; + function initialize(generalInitialization: GeneralInitialization): State { + const initialization: Initialization = Array.isArray(generalInitialization) + ? { scheme: { inputs: generalInitialization } } + : "scheme" in generalInitialization + ? generalInitialization + : { scheme: generalInitialization }; + + const { scheme, dataInit } = initialization; const { inputs, validator } = scheme; const keys = inputs.map((input) => input.key); @@ -185,8 +191,8 @@ export function useInputs(options: { } const values: InputValueDict = {}; - const disabled: InputDisabledDict = clean(info.dataInit?.disabled); - const dirties: InputDirtyDict = clean(info.dataInit?.dirties); + const disabled: InputDisabledDict = clean(dataInit?.disabled); + const dirties: InputDirtyDict = clean(dataInit?.dirties); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; @@ -195,7 +201,7 @@ export function useInputs(options: { values[key] = initializeValue(input, dataInit?.values?.[key]); } - let errors = info.dataInit?.errors; + let errors = dataInit?.errors; if (errors != null) { if (process.env.NODE_ENV === "development") { @@ -331,13 +337,13 @@ export function InputGroup({ )} > {inputs.map((item, index) => { - const { type, value, label, error, helper, disabled } = item; + const { key, type, value, label, error, helper, disabled } = item; const getContainerClassName = ( ...additionalClassNames: classNames.ArgumentArray ) => classNames( - `cru-input-container cru-input-${type}`, + `cru-input-container cru-input-type-${type}`, error && "error", ...additionalClassNames, ); @@ -350,7 +356,7 @@ export function InputGroup({ const { password } = item; return (
{label && } @@ -369,7 +375,7 @@ export function InputGroup({ ); } else if (type === "bool") { return ( -
+
+