Compare commits
	
		
			12 Commits
		
	
	
		
			1.0.1
			...
			fb5a85311c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fb5a85311c | |||
| 288d334615 | |||
| 87f017fc42 | |||
| 43fb8dcda6 | |||
| a3b9c7cdb1 | |||
| c42b8b1bda | |||
| bdcbe94c97 | |||
| caeff985c2 | |||
| bed538793d | |||
| 602f20ad18 | |||
| 457c96b37e | |||
| a3f2b77548 | 
							
								
								
									
										10
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.drone.yml
									
									
									
									
									
								
							| @@ -38,6 +38,7 @@ steps: | |||||||
|     - cd moneymgr_backend |     - cd moneymgr_backend | ||||||
|     - rustup component add clippy |     - rustup component add clippy | ||||||
|     - cargo clippy -- -D warnings |     - cargo clippy -- -D warnings | ||||||
|  |     - cargo clippy --example api_curl -- -D warnings | ||||||
|  |  | ||||||
| - name: backend_test | - name: backend_test | ||||||
|   image: rust |   image: rust | ||||||
| @@ -51,7 +52,7 @@ steps: | |||||||
|     - cargo test |     - cargo test | ||||||
|  |  | ||||||
|  |  | ||||||
| - name: backend_compile | - name: backend_build | ||||||
|   image: rust |   image: rust | ||||||
|   volumes: |   volumes: | ||||||
|   - name: rust_registry |   - name: rust_registry | ||||||
| @@ -67,15 +68,16 @@ steps: | |||||||
|   - cd moneymgr_backend |   - cd moneymgr_backend | ||||||
|   - mv /tmp/web_build/dist static |   - mv /tmp/web_build/dist static | ||||||
|   - cargo build --release |   - cargo build --release | ||||||
|   - ls -lah target/release/moneymgr_backend |   - cargo build --release --example api_curl | ||||||
|   - cp target/release/moneymgr_backend /tmp/release |   - ls -lah target/release/moneymgr_backend target/release/examples/api_curl | ||||||
|  |   - cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release | ||||||
|  |  | ||||||
|  |  | ||||||
| # Release | # Release | ||||||
| - name: gitea_release | - name: gitea_release | ||||||
|   image: plugins/gitea-release |   image: plugins/gitea-release | ||||||
|   depends_on: |   depends_on: | ||||||
|   - backend_compile |   - backend_build | ||||||
|   when: |   when: | ||||||
|     event: |     event: | ||||||
|       - tag |       - tag | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
								
							| @@ -3,10 +3,46 @@ | |||||||
|  |  | ||||||
| Open Source web-based personal expenses tool. | Open Source web-based personal expenses tool. | ||||||
|  |  | ||||||
|  | **Note :** This project does not handle authentication itself. Instead, it relies on OpenID to achieve users authentication. | ||||||
|  |  | ||||||
|  | ## Setup prod env | ||||||
|  | 1. Install prerequisites: | ||||||
|  |    1. docker | ||||||
|  |    2. docker compose | ||||||
|  |    3. git | ||||||
|  |  | ||||||
|  | 2. Clone this git repository: | ||||||
|  | ```bash | ||||||
|  | git clone https://gitea.communiquons.org/pierre/MoneyMgr | ||||||
|  | cd MoneyMgr/docker_prod | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 3. Copy and adapt env values | ||||||
|  | ```bash | ||||||
|  | cp .env.sample .env | ||||||
|  | nano .env | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 4. Create required directories: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | mkdir -p storage/{db,redis-data,redis-conf,minio} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 5. Start containers | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | docker compose up | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | 6. Checkout http://localhost:8000/ | ||||||
|  |  | ||||||
|  | > The default credentials are `admin` / `admin` | ||||||
|  |  | ||||||
| ## Setup dev env | ## Setup dev env | ||||||
| 1. Install prerequisites: | 1. Install prerequisites: | ||||||
|    1. docker |    1. docker | ||||||
|    2. docker-compose |    2. docker compose | ||||||
|    3. rust |    3. rust | ||||||
|    4. node |    4. node | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								docker_prod/.env.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docker_prod/.env.sample
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | MINIO_ROOT_USER=rootuser | ||||||
|  | MINIO_ROOT_PASSWORD=rootpassword | ||||||
|  | DB_USER=db_user | ||||||
|  | DB_PASSWORD=db_password | ||||||
|  | REDIS_PASS=redis_password | ||||||
|  | WEBSITE_ORIGIN=http://localhost:8000 | ||||||
|  | APP_SECRET=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret | ||||||
|  | AUTH_SECRET_KEY=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret | ||||||
|  | OIDC_CLIENT_ID=bar | ||||||
|  | OIDC_CLIENT_SECRET=foo | ||||||
							
								
								
									
										3
									
								
								docker_prod/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								docker_prod/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | .env | ||||||
|  | storage | ||||||
|  | auth/users.json | ||||||
							
								
								
									
										5
									
								
								docker_prod/auth/clients.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								docker_prod/auth/clients.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | - id: ${OIDC_CLIENT_ID} | ||||||
|  |   name: MoneyMgr | ||||||
|  |   description: Money management tool | ||||||
|  |   secret: ${OIDC_CLIENT_SECRET} | ||||||
|  |   redirect_uri: ${APP_ORIGIN}/oidc_cb | ||||||
							
								
								
									
										79
									
								
								docker_prod/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								docker_prod/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | services: | ||||||
|  |   minio: | ||||||
|  |     image: minio/minio | ||||||
|  |     user: "1000" | ||||||
|  |     environment: | ||||||
|  |       - MINIO_ROOT_USER=$MINIO_ROOT_USER | ||||||
|  |       - MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD | ||||||
|  |     volumes: | ||||||
|  |       - ./storage/minio:/data | ||||||
|  |     command: [ "minio", "server", "/data", "--console-address", ":9090" ] | ||||||
|  |     ports: | ||||||
|  |       - 9000:9000 | ||||||
|  |       - 9090:9090 | ||||||
|  |     expose: | ||||||
|  |       - 9000 | ||||||
|  |  | ||||||
|  |   db: | ||||||
|  |     image: postgres | ||||||
|  |     user: "1000" | ||||||
|  |     ports: | ||||||
|  |       - "5432:5432" | ||||||
|  |     expose: | ||||||
|  |       - 5432 | ||||||
|  |     environment: | ||||||
|  |       - POSTGRES_USER=$DB_USER | ||||||
|  |       - POSTGRES_PASSWORD=$DB_PASSWORD | ||||||
|  |       - POSTGRES_DB=moneymgr | ||||||
|  |     volumes: | ||||||
|  |       - ./storage/db:/var/lib/postgresql/data | ||||||
|  |  | ||||||
|  |   oidc: | ||||||
|  |     image: pierre42100/basic_oidc | ||||||
|  |     user: "1000" | ||||||
|  |     environment: | ||||||
|  |       - LISTEN_ADDRESS=0.0.0.0:9001 | ||||||
|  |       - STORAGE_PATH=/storage | ||||||
|  |       - TOKEN_KEY=$AUTH_SECRET_KEY | ||||||
|  |       - WEBSITE_ORIGIN=http://localhost:9001 | ||||||
|  |       - OIDC_CLIENT_ID=$OIDC_CLIENT_ID | ||||||
|  |       - OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET | ||||||
|  |       - APP_ORIGIN=$WEBSITE_ORIGIN | ||||||
|  |     expose: | ||||||
|  |       - 9001 | ||||||
|  |     ports: | ||||||
|  |       - 9001:9001 | ||||||
|  |     volumes: | ||||||
|  |       - ./auth:/storage | ||||||
|  |  | ||||||
|  |   redis: | ||||||
|  |     image: redis:alpine | ||||||
|  |     user: "1000" | ||||||
|  |     command: redis-server --requirepass ${REDIS_PASS:-secretredis} | ||||||
|  |     expose: | ||||||
|  |       - 6379 | ||||||
|  |     volumes: | ||||||
|  |       - ./storage/redis-data:/data | ||||||
|  |       - ./storage/redis-conf:/usr/local/etc/redis/redis.conf | ||||||
|  |  | ||||||
|  |   moneymgr: | ||||||
|  |     image: pierre42100/moneymgr_backend | ||||||
|  |     user: "1000" | ||||||
|  |     ports: | ||||||
|  |       - 8000:8000 | ||||||
|  |     environment: | ||||||
|  |       - WEBSITE_ORIGIN=${WEBSITE_ORIGIN} | ||||||
|  |       - SECRET=${APP_SECRET} | ||||||
|  |       - DB_HOST=db | ||||||
|  |       - DB_USERNAME=$DB_USER | ||||||
|  |       - DB_PASSWORD=$DB_PASSWORD | ||||||
|  |       - DB_NAME=moneymgr | ||||||
|  |       - OIDC_CONFIGURATION_URL=http://oidc:9001/.well-known/openid-configuration | ||||||
|  |       - OIDC_PROVIDER_NAME=OIDC | ||||||
|  |       - OIDC_CLIENT_ID=$OIDC_CLIENT_ID | ||||||
|  |       - OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET | ||||||
|  |       - S3_ENDPOINT=http://minio:9000 | ||||||
|  |       - S3_ACCESS_KEY=$MINIO_ROOT_USER | ||||||
|  |       - S3_SECRET_KEY=$MINIO_ROOT_PASSWORD | ||||||
|  |       - REDIS_HOSTNAME=redis | ||||||
|  |       - REDIS_PASSWORD=${REDIS_PASS:-secretredis} | ||||||
| @@ -7,13 +7,13 @@ edition = "2024" | |||||||
| env_logger = "0.11.8" | env_logger = "0.11.8" | ||||||
| log = "0.4.27" | log = "0.4.27" | ||||||
| diesel = { version = "2.2.10", features = ["postgres", "r2d2"] } | diesel = { version = "2.2.10", features = ["postgres", "r2d2"] } | ||||||
| diesel_migrations = "2.1.0" | diesel_migrations = "2.2.0" | ||||||
| clap = { version = "4.5.38", features = ["env", "derive"] } | clap = { version = "4.5.38", features = ["env", "derive"] } | ||||||
| actix-web = "4.11.0" | actix-web = "4.11.0" | ||||||
| actix-cors = "0.7.1" | actix-cors = "0.7.1" | ||||||
| actix-multipart = "0.7.2" | actix-multipart = "0.7.2" | ||||||
| actix-remote-ip = "0.1.0" | actix-remote-ip = "0.1.0" | ||||||
| actix-session = { version = "0.10.0", features = ["redis-session"] } | actix-session = { version = "0.10.1", features = ["redis-session"] } | ||||||
| actix-files = "0.6.6" | actix-files = "0.6.6" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.5.0" | ||||||
| anyhow = "1.0.98" | anyhow = "1.0.98" | ||||||
| @@ -27,7 +27,7 @@ light-openid = "1.0.4" | |||||||
| rand = "0.9.1" | rand = "0.9.1" | ||||||
| ipnet = { version = "2.11.0", features = ["serde"] } | ipnet = { version = "2.11.0", features = ["serde"] } | ||||||
| lazy-regex = "3.4.1" | lazy-regex = "3.4.1" | ||||||
| jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] } | jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } | ||||||
| mime_guess = "2.0.5" | mime_guess = "2.0.5" | ||||||
| rust-embed = { version = "8.7.2" } | rust-embed = { version = "8.7.2" } | ||||||
| sha2 = "0.11.0-pre.5" | sha2 = "0.11.0-pre.5" | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ use std::process::Command; | |||||||
| struct Args { | struct Args { | ||||||
|     /// URL to Money manager API |     /// URL to Money manager API | ||||||
|     #[arg(short('U'), long, env, default_value = "http://localhost:8000/api")] |     #[arg(short('U'), long, env, default_value = "http://localhost:8000/api")] | ||||||
|     matrix_gw_url: String, |     moneymgr_url: String, | ||||||
|  |  | ||||||
|     /// Token ID |     /// Token ID | ||||||
|     #[arg(short('i'), long, env)] |     #[arg(short('i'), long, env)] | ||||||
| @@ -39,7 +39,8 @@ struct Args { | |||||||
| fn main() { | fn main() { | ||||||
|     let args: Args = Args::parse(); |     let args: Args = Args::parse(); | ||||||
|  |  | ||||||
|     let full_url = format!("{}{}", args.matrix_gw_url, args.uri); |     let full_url = format!("{}{}", args.moneymgr_url, args.uri); | ||||||
|  |  | ||||||
|     log::debug!("Full URL: {full_url}"); |     log::debug!("Full URL: {full_url}"); | ||||||
|  |  | ||||||
|     let key = HS256Key::from_bytes(args.token_secret.as_bytes()); |     let key = HS256Key::from_bytes(args.token_secret.as_bytes()); | ||||||
|   | |||||||
| @@ -78,12 +78,15 @@ pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) - | |||||||
|  |  | ||||||
| /// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file | /// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file | ||||||
| pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult { | pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult { | ||||||
|     let accounts = accounts_service::get_list_user(auth.user_id()).await?; |     let mut accounts = accounts_service::get_list_user(auth.user_id()).await?; | ||||||
|  |     accounts.sort_by_key(|a| a.id()); | ||||||
|  |  | ||||||
|     let mut out = FinancesManagerFile { accounts: vec![] }; |     let mut out = FinancesManagerFile { accounts: vec![] }; | ||||||
|  |  | ||||||
|     for account in accounts { |     for account in accounts { | ||||||
|         let movements = movements_service::get_list_account(account.id()).await?; |         let mut movements = movements_service::get_list_account(account.id()).await?; | ||||||
|  |         movements.sort_by(|a, b| b.time.cmp(&a.time)); | ||||||
|  |  | ||||||
|         let mut file_account = FinancesManagerAccount { |         let mut file_account = FinancesManagerAccount { | ||||||
|             name: account.name, |             name: account.name, | ||||||
|             movements: Vec::with_capacity(movements.len()), |             movements: Vec::with_capacity(movements.len()), | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -27,7 +27,7 @@ | |||||||
|         "react-dom": "^19.1.0", |         "react-dom": "^19.1.0", | ||||||
|         "react-router": "^7.6.0", |         "react-router": "^7.6.0", | ||||||
|         "react-router-dom": "^7.6.0", |         "react-router-dom": "^7.6.0", | ||||||
|         "ts-pattern": "^5.7.0" |         "ts-pattern": "^5.7.1" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@eslint/js": "^9.26.0", |         "@eslint/js": "^9.26.0", | ||||||
| @@ -36,7 +36,7 @@ | |||||||
|         "@vitejs/plugin-react": "^4.4.1", |         "@vitejs/plugin-react": "^4.4.1", | ||||||
|         "eslint": "^9.26.0", |         "eslint": "^9.26.0", | ||||||
|         "eslint-plugin-react-dom": "^1.49.0", |         "eslint-plugin-react-dom": "^1.49.0", | ||||||
|         "eslint-plugin-react-hooks": "^5.1.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^00.4.20", |         "eslint-plugin-react-refresh": "^00.4.20", | ||||||
|         "eslint-plugin-react-x": "^1.49.0", |         "eslint-plugin-react-x": "^1.49.0", | ||||||
|         "globals": "^16.1.0", |         "globals": "^16.1.0", | ||||||
| @@ -5448,9 +5448,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/ts-pattern": { |     "node_modules/ts-pattern": { | ||||||
|       "version": "5.7.0", |       "version": "5.7.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.0.tgz", |       "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz", | ||||||
|       "integrity": "sha512-0/FvIG4g3kNkYgbNwBBW5pZBkfpeYQnH+2AA3xmjkCAit/DSDPKmgwC3fKof4oYUq6gupClVOJlFl+939VRBMg==", |       "integrity": "sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/tslib": { |     "node_modules/tslib": { | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ | |||||||
|     "react-dom": "^19.1.0", |     "react-dom": "^19.1.0", | ||||||
|     "react-router": "^7.6.0", |     "react-router": "^7.6.0", | ||||||
|     "react-router-dom": "^7.6.0", |     "react-router-dom": "^7.6.0", | ||||||
|     "ts-pattern": "^5.7.0" |     "ts-pattern": "^5.7.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@eslint/js": "^9.26.0", |     "@eslint/js": "^9.26.0", | ||||||
| @@ -38,7 +38,7 @@ | |||||||
|     "@vitejs/plugin-react": "^4.4.1", |     "@vitejs/plugin-react": "^4.4.1", | ||||||
|     "eslint": "^9.26.0", |     "eslint": "^9.26.0", | ||||||
|     "eslint-plugin-react-dom": "^1.49.0", |     "eslint-plugin-react-dom": "^1.49.0", | ||||||
|     "eslint-plugin-react-hooks": "^5.1.0", |     "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|     "eslint-plugin-react-refresh": "^00.4.20", |     "eslint-plugin-react-refresh": "^00.4.20", | ||||||
|     "eslint-plugin-react-x": "^1.49.0", |     "eslint-plugin-react-x": "^1.49.0", | ||||||
|     "globals": "^16.1.0", |     "globals": "^16.1.0", | ||||||
|   | |||||||
| @@ -121,13 +121,15 @@ function MovementsTable(p: { | |||||||
|  |  | ||||||
|   const chooseAccount = useSelectAccount(); |   const chooseAccount = useSelectAccount(); | ||||||
|  |  | ||||||
|   const [labelFilter, setLabelFilter] = React.useState(""); |   const [filter, setFilter] = React.useState(""); | ||||||
|  |  | ||||||
|   const filteredList = React.useMemo(() => { |   const filteredList = React.useMemo(() => { | ||||||
|     return p.movements.filter((m) => |     return p.movements.filter( | ||||||
|       m.label.toLowerCase().includes(labelFilter.toLowerCase()) |       (m) => | ||||||
|  |         m.label.toLowerCase().includes(filter.toLowerCase()) || | ||||||
|  |         m.amount.toString().includes(filter) | ||||||
|     ); |     ); | ||||||
|   }, [p.movements, labelFilter]); |   }, [p.movements, filter]); | ||||||
|  |  | ||||||
|   const [rowSelectionModel, setRowSelectionModel] = |   const [rowSelectionModel, setRowSelectionModel] = | ||||||
|     React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() }); |     React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() }); | ||||||
| @@ -382,12 +384,12 @@ function MovementsTable(p: { | |||||||
|     <> |     <> | ||||||
|       <div style={{ display: "flex" }}> |       <div style={{ display: "flex" }}> | ||||||
|         <TextField |         <TextField | ||||||
|           placeholder="Filter by label" |           placeholder="Filter by label or amount" | ||||||
|           variant="standard" |           variant="standard" | ||||||
|           size="small" |           size="small" | ||||||
|           value={labelFilter} |           value={filter} | ||||||
|           onChange={(e) => { |           onChange={(e) => { | ||||||
|             setLabelFilter(e.target.value); |             setFilter(e.target.value); | ||||||
|           }} |           }} | ||||||
|           style={{ padding: "0px", flex: 1 }} |           style={{ padding: "0px", flex: 1 }} | ||||||
|         /> |         /> | ||||||
|   | |||||||
| @@ -158,7 +158,7 @@ function ImportExportModal(p: { | |||||||
|         </CardContent>{" "} |         </CardContent>{" "} | ||||||
|         <CardActions> |         <CardActions> | ||||||
|           <span style={{ flex: 1 }}> |           <span style={{ flex: 1 }}> | ||||||
|             <RouterLink to={p.exportURL}> |             <RouterLink to={p.exportURL} target="_blank"> | ||||||
|               <Button |               <Button | ||||||
|                 startIcon={<DownloadIcon />} |                 startIcon={<DownloadIcon />} | ||||||
|                 variant="outlined" |                 variant="outlined" | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { DatePicker } from "@mui/x-date-pickers"; | import { DateField } from "@mui/x-date-pickers"; | ||||||
| import { dateToTime, timeToDate } from "../../utils/DateUtils"; | import { dateToTime, timeToDate } from "../../utils/DateUtils"; | ||||||
| import { TextFieldVariants } from "@mui/material"; | import { TextFieldVariants } from "@mui/material"; | ||||||
|  |  | ||||||
| @@ -13,13 +13,16 @@ export function DateInput(p: { | |||||||
|   variant?: TextFieldVariants; |   variant?: TextFieldVariants; | ||||||
| }): React.ReactElement { | }): React.ReactElement { | ||||||
|   return ( |   return ( | ||||||
|     <DatePicker |     <DateField | ||||||
|       autoFocus={p.autoFocus} |       autoFocus={p.autoFocus} | ||||||
|       readOnly={p.editable === false} |       readOnly={p.editable === false} | ||||||
|       label={p.label} |       label={p.label} | ||||||
|       slotProps={{ |       slotProps={{ | ||||||
|         field: { ref: p.ref }, |         textField: { | ||||||
|         textField: { variant: p.variant ?? "standard", style: p.style }, |           ref: p.ref, | ||||||
|  |           variant: p.variant ?? "standard", | ||||||
|  |           style: p.style, | ||||||
|  |         }, | ||||||
|       }} |       }} | ||||||
|       value={timeToDate(p.value)} |       value={timeToDate(p.value)} | ||||||
|       onChange={(v) => { |       onChange={(v) => { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user