Settings bugfix, frontend implementation, API CLI template integration
This commit is contained in:
		
							parent
							
								
									10f7025569
								
							
						
					
					
						commit
						98fcd2822c
					
				| @ -17,8 +17,13 @@ class StringType extends Parameter { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     // as long as it's numeric or bool, we can safely cast it to a string
 | ||||
|     if (!is_string($value)) { | ||||
|       return false; | ||||
|       if (is_bool($value) || is_int($value) || is_float($value)) { | ||||
|         $this->value = strval($value); | ||||
|       } else { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if ($this->maxLength > 0 && strlen($value) > $this->maxLength) { | ||||
|  | ||||
| @ -129,6 +129,8 @@ abstract class Request { | ||||
|   } | ||||
| 
 | ||||
|   protected abstract function _execute(): bool; | ||||
| 
 | ||||
|   // TODO: replace this function with two abstract methods: getDefaultPermittedGroups and getDescription
 | ||||
|   public static function getDefaultACL(Insert $insert): void { } | ||||
| 
 | ||||
|   protected function check2FA(?TwoFactorToken $tfaToken = null): bool { | ||||
|  | ||||
| @ -13,6 +13,7 @@ namespace Core\API { | ||||
| 
 | ||||
| namespace Core\API\Settings { | ||||
| 
 | ||||
|   use Core\API\Parameter\ArrayType; | ||||
|   use Core\API\Parameter\Parameter; | ||||
|   use Core\API\Parameter\StringType; | ||||
|   use Core\API\SettingsAPI; | ||||
| @ -20,8 +21,6 @@ namespace Core\API\Settings { | ||||
|   use Core\Driver\SQL\Column\Column; | ||||
|   use Core\Driver\SQL\Condition\CondBool; | ||||
|   use Core\Driver\SQL\Condition\CondIn; | ||||
|   use Core\Driver\SQL\Condition\CondNot; | ||||
|   use Core\Driver\SQL\Condition\CondRegex; | ||||
|   use Core\Driver\SQL\Query\Insert; | ||||
|   use Core\Driver\SQL\Strategy\UpdateStrategy; | ||||
|   use Core\Objects\Context; | ||||
| @ -57,7 +56,7 @@ namespace Core\API\Settings { | ||||
|   class Set extends SettingsAPI { | ||||
|     public function __construct(Context $context, bool $externalCall = false) { | ||||
|       parent::__construct($context, $externalCall, array( | ||||
|         'settings' => new Parameter('settings', Parameter::TYPE_ARRAY) | ||||
|         'settings' => new ArrayType("settings", Parameter::TYPE_MIXED) | ||||
|       )); | ||||
|     } | ||||
| 
 | ||||
| @ -75,13 +74,13 @@ namespace Core\API\Settings { | ||||
|       $keys = array(); | ||||
|       $deleteKeys = array(); | ||||
| 
 | ||||
|       foreach($values as $key => $value) { | ||||
|       foreach ($values as $key => $value) { | ||||
|         if (!$paramKey->parseParam($key)) { | ||||
|           $key = print_r($key, true); | ||||
|           return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")"); | ||||
|         } else if(!is_null($value) && !$paramValue->parseParam($value)) { | ||||
|         } else if (!is_null($value) && !$paramValue->parseParam($value)) { | ||||
|           $value = print_r($value, true); | ||||
|           return $this->createError("Invalid Type for value in parameter settings: '$value' (Required: " . $paramValue->getTypeName() . ")"); | ||||
|           return $this->createError("Invalid Type for value in parameter settings for key '$key': '$value' (Required: " . $paramValue->getTypeName() . ")"); | ||||
|         } else if(preg_match("/^[a-zA-Z_][a-zA-Z_0-9-]*$/", $paramKey->value) !== 1) { | ||||
|           return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes"); | ||||
|         } else { | ||||
| @ -91,6 +90,8 @@ namespace Core\API\Settings { | ||||
|             $deleteKeys[] = $paramKey->value; | ||||
|           } | ||||
|           $keys[] = $paramKey->value; | ||||
|           $paramKey->reset(); | ||||
|           $paramValue->reset(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
| @ -103,7 +104,7 @@ namespace Core\API\Settings { | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!empty($deleteKeys) && !$this->deleteKeys($keys)) { | ||||
|       if (!empty($deleteKeys) && !$this->deleteKeys($deleteKeys)) { | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <?php | ||||
| 
 | ||||
| return [ | ||||
|   "settings" => "Einstellungen", | ||||
|   "title" => "Einstellungen", | ||||
|   "information" => "Informationen", | ||||
| 
 | ||||
|   # API Key
 | ||||
| @ -21,5 +21,40 @@ return [ | ||||
|                               "Unter Android kannst du den Google Authenticator benutzen.", | ||||
|   "register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts", | ||||
|   "remove_2fa" => "2FA-Token entfernen", | ||||
|   "remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen" | ||||
|   "remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen", | ||||
| 
 | ||||
|   # settings
 | ||||
|   "key" => "Schlüssel", | ||||
|   "value" => "Wert", | ||||
|   "general" => "Allgemein", | ||||
|   "mail" => "Mail", | ||||
|   "recaptcha" => "reCaptcha", | ||||
|   "uncategorized" => "Unkategorisiert", | ||||
|   "unchanged" => "Unverändert", | ||||
| 
 | ||||
|   # general settings
 | ||||
|   "site_name" => "Seitenname", | ||||
|   "base_url" => "Basis URL", | ||||
|   "user_registration_enabled" => "Benutzerregistrierung erlauben", | ||||
|   "allowed_extensions" => "Erlaubte Dateierweiterungen", | ||||
|   "time_zone" => "Zeitzone", | ||||
| 
 | ||||
|   # mail settings
 | ||||
|   "mail_enabled" => "E-Mail Versand aktiviert", | ||||
|   "mail_from" => "Absender E-Mailadresse", | ||||
|   "mail_host" => "Mail-Server Host", | ||||
|   "mail_port" => "Mail-Server Port", | ||||
|   "mail_username" => "Mail-Server Benutzername", | ||||
|   "mail_password" => "Mail-Server Passwort", | ||||
|   "mail_footer" => "Pfad zum E-Mail-Footer", | ||||
| 
 | ||||
|   # recaptcha
 | ||||
|   "recaptcha_enabled" => "Aktiviere Google reCaptcha", | ||||
|   "recaptcha_public_key" => "reCaptcha öffentlicher Schlüssel", | ||||
|   "recaptcha_private_key" => "reCaptcha privater Schlüssel", | ||||
| 
 | ||||
|   # dialog
 | ||||
|   "fetch_settings_error" => "Fehler beim Holen der Einstellungen", | ||||
|   "save_settings_success" => "Einstellungen erfolgreich gespeichert", | ||||
|   "save_settings_error" => "Fehler beim Speichern der Einstellungen", | ||||
| ]; | ||||
| @ -1,7 +1,7 @@ | ||||
| <?php | ||||
| 
 | ||||
| return [ | ||||
|   "settings" => "Settings", | ||||
|   "title" => "Settings", | ||||
|   "information" => "Information", | ||||
| 
 | ||||
|   # API Key
 | ||||
| @ -22,4 +22,39 @@ return [ | ||||
|   "register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.", | ||||
|   "remove_2fa" => "Remove 2FA Token", | ||||
|   "remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token", | ||||
| 
 | ||||
|   # settings
 | ||||
|   "key" => "Key", | ||||
|   "value" => "Value", | ||||
|   "general" => "General", | ||||
|   "mail" => "Mail", | ||||
|   "recaptcha" => "reCaptcha", | ||||
|   "uncategorized" => "Uncategorized", | ||||
|   "unchanged" => "Unchanged", | ||||
| 
 | ||||
|   # general settings
 | ||||
|   "site_name" => "Site Name", | ||||
|   "base_url" => "Base URL", | ||||
|   "user_registration_enabled" => "Allow user registration", | ||||
|   "allowed_extensions" => "Allowed file extensions", | ||||
|   "time_zone" => "Time zone", | ||||
| 
 | ||||
|   # mail settings
 | ||||
|   "mail_enabled" => "Enable e-mail transport", | ||||
|   "mail_from" => "Sender e-mail address", | ||||
|   "mail_host" => "Mail server host", | ||||
|   "mail_port" => "Mail server port", | ||||
|   "mail_username" => "Mail server username", | ||||
|   "mail_password" => "Mail server password", | ||||
|   "mail_footer" => "Path to e-mail footer", | ||||
| 
 | ||||
|   # recaptcha
 | ||||
|   "recaptcha_enabled" => "Enable Google reCaptcha", | ||||
|   "recaptcha_public_key" => "reCaptcha Public Key", | ||||
|   "recaptcha_private_key" => "reCaptcha Private Key", | ||||
| 
 | ||||
|   # dialog
 | ||||
|   "fetch_settings_error" => "Error fetching settings", | ||||
|   "save_settings_success" => "Settings saved successfully", | ||||
|   "save_settings_error" => "Error saving settings", | ||||
| ]; | ||||
							
								
								
									
										34
									
								
								README.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										34
									
								
								README.md
									
									
									
									
									
								
							| @ -165,10 +165,12 @@ To access and view any frontend pages, the internal router is used. Available ro | ||||
| A static route targets a file, usually located in [/static](/static) and does nothing more, than returning its content. A dynamic route is usually the way to go: | ||||
| It takes two parameters, firstly the target document and secondly, an optional view. For example, take the following routing table: | ||||
| 
 | ||||
| | Route                     | Action | Target | Extra   | | ||||
| |---------------------------| ------ | ------ |---------| | ||||
| | `/funnyCatImage`          | `Serve Static` | `/static/cat.jpg` |         | | ||||
| | `/someRoute/{param:str?}` | `Redirect Dynamic` | `\Documents\MyDocument\` | `param` | | ||||
| | Route                     | Action     | Target                       | Extra   | | ||||
| |---------------------------|------------|------------------------------|---------| | ||||
| | `/funnyCatImage`          | `Static`   | `/static/cat.jpg`            |         | | ||||
| | `/someRoute/{param:str?}` | `Dynamic`  | `\Site\Documents\MyDocument` | `param` | | ||||
| | `/redirectMe`             | `Redirect` | `https://romanh.de/`         |         | | ||||
| 
 | ||||
| 
 | ||||
| The first route would return the cat image, if the case-insensitive path `/funnyCatImage` is requested. | ||||
| The second route is more interesting, as it includes an optional parameter of type string, which means, any route starting with `/someRoute/` or just `/someRoute` is accepted. | ||||
| @ -182,12 +184,12 @@ we could have one document, when showing different articles and products, and a | ||||
| To create a new document, a class inside [/Core/Documents](/Core/Documents) is created with the following scheme: | ||||
| 
 | ||||
| ```php | ||||
| namespace Documents { | ||||
| namespace Site\Documents { | ||||
| 
 | ||||
|   use Elements\Document; | ||||
|   use Objects\Router\Router; | ||||
|   use Documents\Example\ExampleHead; | ||||
|   use Documents\Example\ExampleBody; | ||||
|   use Core\Elements\Document; | ||||
|   use Core\Objects\Router\Router; | ||||
|   use Site\Documents\Example\ExampleHead; | ||||
|   use Site\Documents\Example\ExampleBody; | ||||
| 
 | ||||
|   class ExampleDocument extends Document { | ||||
|     public function __construct(Router $router, ?string $view = NULL) { | ||||
| @ -196,10 +198,10 @@ namespace Documents { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| namespace Documents\Example { | ||||
| namespace Site\Documents\Example { | ||||
| 
 | ||||
|   use Elements\Head; | ||||
|   use Elements\Body; | ||||
|   use Core\Elements\Head; | ||||
|   use Core\Elements\Body; | ||||
| 
 | ||||
|   class ExampleHead extends Head { | ||||
|    | ||||
| @ -255,7 +257,7 @@ we firstly have to load the module. This is done by adding the class, or the obj | ||||
| To translate the defined strings, we can use the global `L()` function. The following code snipped shows the use of  | ||||
| our sample language module: | ||||
| 
 | ||||
| **/Core/Localization/de_DE/example.php**: | ||||
| **/Site/Localization/de_DE/example.php**: | ||||
| ```php | ||||
| <?php | ||||
| return [ | ||||
| @ -316,6 +318,12 @@ php cli.php frontend rm <module-name> | ||||
| php cli.php frontend dev <module-name> | ||||
| ``` | ||||
| 
 | ||||
| ### API commands | ||||
| ```bash | ||||
| php cli.php api ls | ||||
| php cli.php api <add> # interactive wizard | ||||
| ``` | ||||
| 
 | ||||
| ## Project Structure | ||||
| ``` | ||||
| ├── Core                     | ||||
|  | ||||
							
								
								
									
										10
									
								
								cli.php
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										10
									
								
								cli.php
									
									
									
									
									
								
							| @ -859,6 +859,10 @@ function onAPI(array $argv): void { | ||||
|       // TODO: auto-generated method stub
 | ||||
|       return \$this->success; | ||||
|     } | ||||
| 
 | ||||
|     public static function getDefaultACL(Insert \$insert): void { | ||||
|       \$insert->addRow(self::getEndpoint(), [], \"Short description, what users are allowed to do with this permission\", false);
 | ||||
|     } | ||||
|   }";
 | ||||
|       }, $methodNames)); | ||||
|       $content = "<?php
 | ||||
| @ -867,6 +871,7 @@ namespace Site\API { | ||||
|    | ||||
|   use Core\API\Request; | ||||
|   use Core\Objects\Context; | ||||
|   use Core\Driver\SQL\Query\Insert; | ||||
|    | ||||
|   abstract class {$apiName}API extends Request { | ||||
|     public function __construct(Context \$context, bool \$externalCall = false, array \$params = []) { | ||||
| @ -891,6 +896,7 @@ namespace Site\API; | ||||
| 
 | ||||
| use Core\API\Request; | ||||
| use Core\Objects\Context; | ||||
| use Core\Driver\SQL\Query\Insert; | ||||
| 
 | ||||
| class $apiName extends Request { | ||||
| 
 | ||||
| @ -903,6 +909,10 @@ class $apiName extends Request { | ||||
|     // TODO: auto-generated method stub
 | ||||
|     return \$this->success; | ||||
|   } | ||||
|    | ||||
|   public static function getDefaultACL(Insert \$insert): void { | ||||
|     \$insert->addRow(self::getEndpoint(), [], \"Short description, what users are allowed to do with this permission\", false);
 | ||||
|   } | ||||
| } | ||||
| ";
 | ||||
|     } | ||||
|  | ||||
| @ -22,6 +22,7 @@ const LogView = lazy(() => import("./views/log-view")); | ||||
| const AccessControlList = lazy(() => import("./views/access-control-list")); | ||||
| const RouteListView = lazy(() => import("./views/route/route-list")); | ||||
| const RouteEditView = lazy(() => import("./views/route/route-edit")); | ||||
| const SettingsView = lazy(() => import("./views/settings")); | ||||
| 
 | ||||
| export default function AdminDashboard(props) { | ||||
| 
 | ||||
| @ -83,6 +84,7 @@ export default function AdminDashboard(props) { | ||||
|                             <Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/> | ||||
|                             <Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/> | ||||
|                             <Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/> | ||||
|                             <Route path={"/admin/settings"} element={<SettingsView {...controlObj} />}/> | ||||
|                             <Route path={"*"} element={<View404 />} /> | ||||
|                         </Routes> | ||||
|                     </Suspense> | ||||
|  | ||||
| @ -219,7 +219,8 @@ export default function EditGroupView(props) { | ||||
|                                 onClick={() => navigate("/admin/groups")}> | ||||
|                             {L("general.go_back")} | ||||
|                         </Button> | ||||
|                         <Button startIcon={<Save />} color={"primary"} | ||||
|                         <Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||
|                                 color={"primary"} | ||||
|                                 variant={"outlined"} | ||||
|                                 disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))} | ||||
|                                 onClick={onSave}> | ||||
|  | ||||
| @ -138,8 +138,10 @@ export default function RouteEditView(props) { | ||||
|                     onClick={() => navigate("/admin/routes")}> | ||||
|                 {L("general.cancel")} | ||||
|             </Button> | ||||
|             <Button startIcon={<Save />} color={"primary"} | ||||
|                     variant={"outlined"} disabled={isSaving} | ||||
|             <Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||
|                     color={"primary"} | ||||
|                     variant={"outlined"} | ||||
|                     disabled={isSaving} | ||||
|                     onClick={onSave}> | ||||
|                 {isSaving ? L("general.saving") + "…" : L("general.save")} | ||||
|             </Button> | ||||
|  | ||||
							
								
								
									
										367
									
								
								react/admin-panel/src/views/settings.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										367
									
								
								react/admin-panel/src/views/settings.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,367 @@ | ||||
| import {useCallback, useContext, useEffect, useState} from "react"; | ||||
| import {LocaleContext} from "shared/locale"; | ||||
| import { | ||||
|     Box, Button, Checkbox, | ||||
|     CircularProgress, FormControl, FormControlLabel, | ||||
|     FormGroup, FormLabel, IconButton, | ||||
|     Paper, Select, styled, | ||||
|     Tab, | ||||
|     Table, | ||||
|     TableBody, | ||||
|     TableCell, | ||||
|     TableHead, | ||||
|     TableRow, | ||||
|     Tabs, TextField | ||||
| } from "@mui/material"; | ||||
| import {Link} from "react-router-dom"; | ||||
| import {Add, Delete, Google, LibraryBooks, Mail, RestartAlt, Save, SettingsApplications} from "@mui/icons-material"; | ||||
| import {TableContainer} from "@material-ui/core"; | ||||
| import TIME_ZONES from "shared/time-zones"; | ||||
| 
 | ||||
| const SettingsFormGroup = styled(FormGroup)((props) => ({ | ||||
|     marginBottom: props.theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const ButtonBar = styled(Box)((props) => ({ | ||||
|     "& > button": { | ||||
|         marginRight: props.theme.spacing(1) | ||||
|     } | ||||
| })); | ||||
| 
 | ||||
| export default function SettingsView(props) { | ||||
| 
 | ||||
|     // meta
 | ||||
|     const api = props.api; | ||||
|     const showDialog = props.showDialog; | ||||
|     const {translate: L, requestModules, currentLocale} = useContext(LocaleContext); | ||||
|     const KNOWN_SETTING_KEYS = { | ||||
|       "general": [ | ||||
|           "base_url", | ||||
|           "site_name", | ||||
|           "user_registration_enabled", | ||||
|           "time_zone", | ||||
|           "allowed_extensions", | ||||
|       ], | ||||
|       "mail": [ | ||||
|           "mail_enabled", | ||||
|           "mail_footer", | ||||
|           "mail_from", | ||||
|           "mail_host", | ||||
|           "mail_port", | ||||
|           "mail_username", | ||||
|           "mail_password", | ||||
|       ], | ||||
|       "recaptcha": [ | ||||
|           "recaptcha_enabled", | ||||
|           "recaptcha_private_key", | ||||
|           "recaptcha_public_key", | ||||
|       ], | ||||
|       "hidden": ["installation_completed", "mail_last_sync"] | ||||
|     }; | ||||
| 
 | ||||
|     // data
 | ||||
|     const [fetchSettings, setFetchSettings] = useState(true); | ||||
|     const [settings, setSettings] = useState(null); | ||||
|     const [uncategorizedKeys, setUncategorizedKeys] = useState([]); | ||||
| 
 | ||||
|     // ui
 | ||||
|     const [selectedTab, setSelectedTab] = useState("general"); | ||||
|     const [hasChanged, setChanged] = useState(false); | ||||
|     const [isSaving, setSaving] = useState(false); | ||||
|     const [newKey, setNewKey] = useState(""); | ||||
| 
 | ||||
|     const isUncategorized = (key) => { | ||||
|         return !(Object.values(KNOWN_SETTING_KEYS).reduce((acc, arr) => { | ||||
|             return [ ...acc, ...arr ]; | ||||
|         }, [])).includes(key); | ||||
|     } | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         requestModules(props.api, ["general", "settings"], currentLocale).then(data => { | ||||
|             if (!data.success) { | ||||
|                 showDialog("Error fetching translations: " + data.msg); | ||||
|             } | ||||
|         }); | ||||
|     }, [currentLocale]); | ||||
| 
 | ||||
|     const onFetchSettings = useCallback((force = false) => { | ||||
|         if (fetchSettings || force) { | ||||
|             setFetchSettings(false); | ||||
|             api.getSettings().then(data => { | ||||
|                 if (!data.success) { | ||||
|                     showDialog(data.msg, L("settings.fetch_settings_error")); | ||||
|                 } else { | ||||
|                     setSettings(Object.keys(data.settings) | ||||
|                         .filter(key => !KNOWN_SETTING_KEYS.hidden.includes(key)) | ||||
|                         .reduce((obj, key) => { | ||||
|                             obj[key] = data.settings[key]; | ||||
|                             return obj; | ||||
|                         }, {}) | ||||
|                     ); | ||||
|                     setUncategorizedKeys(Object.keys(data.settings).filter(key => isUncategorized(key))); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }, [api, showDialog, fetchSettings]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         onFetchSettings(); | ||||
|     }, [fetchSettings]); | ||||
| 
 | ||||
|     const onChangeValue = useCallback((key, value) => { | ||||
|         setChanged(true); | ||||
|         setSettings({...settings, [key]: value}); | ||||
|     }, [settings]); | ||||
| 
 | ||||
|     const onSaveSettings = useCallback(() => { | ||||
|         setSaving(true); | ||||
|         api.saveSettings(settings).then(data => { | ||||
|             setSaving(false); | ||||
|             if (data.success) { | ||||
|                 showDialog(L("settings.save_settings_success"), L("general.success")); | ||||
|                 setChanged(false); | ||||
|             } else { | ||||
|                 showDialog(data.msg, L("settings.save_settings_error")); | ||||
|             } | ||||
|         }); | ||||
|     }, [api, showDialog, settings]); | ||||
| 
 | ||||
|     const onDeleteKey = useCallback(key => { | ||||
|         if (key && settings.hasOwnProperty(key)) { | ||||
|             let index = uncategorizedKeys.indexOf(key); | ||||
|             if (index !== -1) { | ||||
|                 let newUncategorizedKeys = [...uncategorizedKeys]; | ||||
|                 newUncategorizedKeys.splice(index, 1); | ||||
|                 setUncategorizedKeys(newUncategorizedKeys); | ||||
|             } | ||||
|             setChanged(true); | ||||
|             setSettings({...settings, [key]: null}); | ||||
|         } | ||||
|     }, [settings, uncategorizedKeys]); | ||||
| 
 | ||||
|     const onAddKey = useCallback(key => { | ||||
|         if (key) { | ||||
|             if (!isUncategorized(key) || !settings.hasOwnProperty(key) || settings[key] === null) { | ||||
|                 setChanged(true); | ||||
|                 setSettings({...settings, [key]: ""}); | ||||
|                 setUncategorizedKeys([...uncategorizedKeys, key]); | ||||
|                 setNewKey(""); | ||||
|             } else { | ||||
|                 showDialog("This key is already defined", L("general.error")); | ||||
|             } | ||||
|         } | ||||
|     }, [settings, uncategorizedKeys, showDialog]); | ||||
| 
 | ||||
|     const onChangeKey = useCallback((oldKey, newKey) => { | ||||
|         if (settings.hasOwnProperty(oldKey) && !settings.hasOwnProperty(newKey)) { | ||||
|             let newSettings = {...settings, [newKey]: settings[oldKey]}; | ||||
|             delete newSettings[oldKey]; | ||||
|             setChanged(true); | ||||
|             setSettings(newSettings); | ||||
|         } | ||||
|     }, [settings]); | ||||
| 
 | ||||
|     if (settings === null) { | ||||
|         return <CircularProgress /> | ||||
|     } | ||||
| 
 | ||||
|     const parseBool = (v) => v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()); | ||||
| 
 | ||||
|     const renderTextInput = (key_name, disabled=false, props={}) => { | ||||
|         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||
|             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||
|             <FormControl> | ||||
|                 <TextField size={"small"} variant={"outlined"} | ||||
|                            disabled={disabled} | ||||
|                            value={settings[key_name]} | ||||
|                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||
|             </FormControl> | ||||
|         </SettingsFormGroup> | ||||
|     } | ||||
| 
 | ||||
|     const renderPasswordInput = (key_name, disabled=false, props={}) => { | ||||
|         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||
|             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||
|             <FormControl> | ||||
|                 <TextField size={"small"} variant={"outlined"} | ||||
|                            type={"password"} | ||||
|                            disabled={disabled} | ||||
|                            placeholder={"(" + L("settings.unchanged") + ")"} | ||||
|                            value={settings[key_name]} | ||||
|                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||
|             </FormControl> | ||||
|         </SettingsFormGroup> | ||||
|     } | ||||
| 
 | ||||
|     const renderNumberInput = (key_name, minValue, maxValue, disabled=false, props={}) => { | ||||
|         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||
|             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||
|             <FormControl> | ||||
|                 <TextField size={"small"} variant={"outlined"} | ||||
|                            type={"number"} | ||||
|                            disabled={disabled} | ||||
|                            inputProps={{min: minValue, max: maxValue}} | ||||
|                            value={settings[key_name]} | ||||
|                            onChange={e => onChangeValue(key_name, e.target.value)} /> | ||||
|             </FormControl> | ||||
|         </SettingsFormGroup> | ||||
|     } | ||||
| 
 | ||||
|     const renderCheckBox = (key_name, disabled=false, props={}) => { | ||||
|         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||
|             <FormControlLabel | ||||
|                 disabled={disabled} | ||||
|                 control={<Checkbox | ||||
|                     disabled={disabled} | ||||
|                     checked={parseBool(settings[key_name])} | ||||
|                     onChange={(e, v) => onChangeValue(key_name, v)} />} | ||||
|                 label={L("settings." + key_name)} /> | ||||
|         </SettingsFormGroup> | ||||
|     } | ||||
| 
 | ||||
|     const renderSelection = (key_name, options, disabled=false, props={}) => { | ||||
|         return <SettingsFormGroup key={"form-" + key_name} {...props}> | ||||
|             <FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel> | ||||
|             <FormControl> | ||||
|                 <Select native value={settings[key_name]} | ||||
|                         disabled={disabled} | ||||
|                         size={"small"} onChange={e => onChangeValue(key_name, e.target.value)}> | ||||
|                         {options.map(option => <option | ||||
|                             key={"option-" + option} | ||||
|                             value={option}> | ||||
|                                 {option} | ||||
|                         </option>)} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|         </SettingsFormGroup> | ||||
|     } | ||||
| 
 | ||||
|     const renderTab = () => { | ||||
|         if (selectedTab === "general") { | ||||
|             return [ | ||||
|                 renderTextInput("site_name"), | ||||
|                 renderTextInput("base_url"), | ||||
|                 renderCheckBox("user_registration_enabled"), | ||||
|                 renderTextInput("allowed_extensions"), | ||||
|                 renderSelection("time_zone", TIME_ZONES), | ||||
|             ]; | ||||
|         } else if (selectedTab === "mail") { | ||||
|             return [ | ||||
|                 renderCheckBox("mail_enabled"), | ||||
|                 renderTextInput("mail_from", !parseBool(settings.mail_enabled)), | ||||
|                 renderTextInput("mail_host", !parseBool(settings.mail_enabled)), | ||||
|                 renderNumberInput("mail_port", 1, 65535, !parseBool(settings.mail_enabled)), | ||||
|                 renderTextInput("mail_username", !parseBool(settings.mail_enabled)), | ||||
|                 renderPasswordInput("mail_password", !parseBool(settings.mail_enabled)), | ||||
|                 renderTextInput("mail_footer", !parseBool(settings.mail_enabled)), | ||||
|             ]; | ||||
|         } else if (selectedTab === "recaptcha") { | ||||
|             return [ | ||||
|                 renderCheckBox("recaptcha_enabled"), | ||||
|                 renderTextInput("recaptcha_public_key", !parseBool(settings.recaptcha_enabled)), | ||||
|                 renderPasswordInput("recaptcha_private_key", !parseBool(settings.recaptcha_enabled)), | ||||
|             ]; | ||||
|         } else if (selectedTab === "uncategorized") { | ||||
|             return <TableContainer component={Paper}> | ||||
|                     <Table> | ||||
|                         <TableHead> | ||||
|                             <TableRow> | ||||
|                                 <TableCell>{L("settings.key")}</TableCell> | ||||
|                                 <TableCell>{L("settings.value")}</TableCell> | ||||
|                                 <TableCell align={"center"}>{L("general.controls")}</TableCell> | ||||
|                             </TableRow> | ||||
|                         </TableHead> | ||||
|                         <TableBody> | ||||
|                             {uncategorizedKeys.map(key => <TableRow key={"settings-" + key}> | ||||
|                                 <TableCell> | ||||
|                                     <TextField fullWidth={true} size={"small"} value={key} | ||||
|                                                onChange={e => onChangeKey(key, e.target.value)} /> | ||||
|                                 </TableCell> | ||||
|                                 <TableCell> | ||||
|                                     <TextField fullWidth={true} size={"small"} value={settings[key]} | ||||
|                                         onChange={e => onChangeValue(key, e.target.value)} /> | ||||
|                                 </TableCell> | ||||
|                                 <TableCell align={"center"}> | ||||
|                                     <IconButton onClick={() => onDeleteKey(key)} | ||||
|                                         color={"secondary"}> | ||||
|                                         <Delete /> | ||||
|                                     </IconButton> | ||||
|                                 </TableCell> | ||||
|                             </TableRow>)} | ||||
|                             <TableRow> | ||||
|                                 <TableCell> | ||||
|                                     <TextField fullWidth={true} size={"small"} onChange={e => setNewKey(e.target.value)} | ||||
|                                         onBlur={() => onAddKey(newKey)} value={newKey}/> | ||||
|                                 </TableCell> | ||||
|                                 <TableCell> | ||||
|                                     <TextField fullWidth={true} size={"small"} /> | ||||
|                                 </TableCell> | ||||
|                                 <TableCell align={"center"}> | ||||
|                                 </TableCell> | ||||
|                             </TableRow> | ||||
|                         </TableBody> | ||||
|                     </Table> | ||||
|                     <Box p={1}> | ||||
|                         <Button startIcon={<Add />} variant={"outlined"} | ||||
|                                 size={"small"}> | ||||
|                                     {L("general.add")} | ||||
|                                 </Button> | ||||
|                     </Box> | ||||
|                 </TableContainer> | ||||
|         } else { | ||||
|             return <i>Invalid tab: {selectedTab}</i> | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return <> | ||||
|         <div className={"content-header"}> | ||||
|             <div className={"container-fluid"}> | ||||
|                 <div className={"row mb-2"}> | ||||
|                     <div className={"col-sm-6"}> | ||||
|                         <h1 className={"m-0 text-dark"}>{L("settings.title")}</h1> | ||||
|                     </div> | ||||
|                     <div className={"col-sm-6"}> | ||||
|                         <ol className={"breadcrumb float-sm-right"}> | ||||
|                             <li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> | ||||
|                             <li className="breadcrumb-item active">{L("settings.title")}</li> | ||||
|                         </ol> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div className={"content"}> | ||||
|             <Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}> | ||||
|                 <Tab value={"general"} label={L("settings.general")} | ||||
|                      icon={<SettingsApplications />} iconPosition={"start"} /> | ||||
|                 <Tab value={"mail"} label={L("settings.mail")} | ||||
|                      icon={<Mail />} iconPosition={"start"} /> | ||||
|                 <Tab value={"recaptcha"} label={L("settings.recaptcha")} | ||||
|                      icon={<Google />} iconPosition={"start"} /> | ||||
|                 <Tab value={"uncategorized"} label={L("settings.uncategorized")} | ||||
|                      icon={<LibraryBooks />} iconPosition={"start"} /> | ||||
|             </Tabs> | ||||
|             <Box p={2}> | ||||
|             { | ||||
|                 renderTab() | ||||
|             } | ||||
|             </Box> | ||||
|             <ButtonBar> | ||||
|                 <Button color={"primary"} | ||||
|                         onClick={onSaveSettings} | ||||
|                         disabled={isSaving} | ||||
|                         startIcon={isSaving ? <CircularProgress size={14} /> : <Save />} | ||||
|                         variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}> | ||||
|                     {isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))} | ||||
|                 </Button> | ||||
|                 <Button color={"secondary"} | ||||
|                         onClick={() => { setFetchSettings(true); setNewKey(""); }} | ||||
|                         disabled={isSaving} | ||||
|                         startIcon={<RestartAlt />} | ||||
|                         variant={"outlined"} title={L("general.reset")}> | ||||
|                     {L("general.reset")} | ||||
|                 </Button> | ||||
|             </ButtonBar> | ||||
|         </div> | ||||
|     </> | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										603
									
								
								react/shared/time-zones.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										603
									
								
								react/shared/time-zones.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,603 @@ | ||||
| // Source: https://pecl.php.net/get/timezonedb
 | ||||
| // Date: 2024-04-05 12:00:00
 | ||||
| const TIME_ZONES = [ | ||||
|     "Africa/Abidjan", | ||||
|     "Africa/Accra", | ||||
|     "Africa/Addis_Ababa", | ||||
|     "Africa/Algiers", | ||||
|     "Africa/Asmara", | ||||
|     "Africa/Asmera", | ||||
|     "Africa/Bamako", | ||||
|     "Africa/Bangui", | ||||
|     "Africa/Banjul", | ||||
|     "Africa/Bissau", | ||||
|     "Africa/Blantyre", | ||||
|     "Africa/Brazzaville", | ||||
|     "Africa/Bujumbura", | ||||
|     "Africa/Cairo", | ||||
|     "Africa/Casablanca", | ||||
|     "Africa/Ceuta", | ||||
|     "Africa/Conakry", | ||||
|     "Africa/Dakar", | ||||
|     "Africa/Dar_es_Salaam", | ||||
|     "Africa/Djibouti", | ||||
|     "Africa/Douala", | ||||
|     "Africa/El_Aaiun", | ||||
|     "Africa/Freetown", | ||||
|     "Africa/Gaborone", | ||||
|     "Africa/Harare", | ||||
|     "Africa/Johannesburg", | ||||
|     "Africa/Juba", | ||||
|     "Africa/Kampala", | ||||
|     "Africa/Khartoum", | ||||
|     "Africa/Kigali", | ||||
|     "Africa/Kinshasa", | ||||
|     "Africa/Lagos", | ||||
|     "Africa/Libreville", | ||||
|     "Africa/Lome", | ||||
|     "Africa/Luanda", | ||||
|     "Africa/Lubumbashi", | ||||
|     "Africa/Lusaka", | ||||
|     "Africa/Malabo", | ||||
|     "Africa/Maputo", | ||||
|     "Africa/Maseru", | ||||
|     "Africa/Mbabane", | ||||
|     "Africa/Mogadishu", | ||||
|     "Africa/Monrovia", | ||||
|     "Africa/Nairobi", | ||||
|     "Africa/Ndjamena", | ||||
|     "Africa/Niamey", | ||||
|     "Africa/Nouakchott", | ||||
|     "Africa/Ouagadougou", | ||||
|     "Africa/Porto-Novo", | ||||
|     "Africa/Sao_Tome", | ||||
|     "Africa/Timbuktu", | ||||
|     "Africa/Tripoli", | ||||
|     "Africa/Tunis", | ||||
|     "Africa/Windhoek", | ||||
|     "America/Adak", | ||||
|     "America/Anchorage", | ||||
|     "America/Anguilla", | ||||
|     "America/Antigua", | ||||
|     "America/Araguaina", | ||||
|     "America/Argentina/Buenos_Aires", | ||||
|     "America/Argentina/Catamarca", | ||||
|     "America/Argentina/ComodRivadavia", | ||||
|     "America/Argentina/Cordoba", | ||||
|     "America/Argentina/Jujuy", | ||||
|     "America/Argentina/La_Rioja", | ||||
|     "America/Argentina/Mendoza", | ||||
|     "America/Argentina/Rio_Gallegos", | ||||
|     "America/Argentina/Salta", | ||||
|     "America/Argentina/San_Juan", | ||||
|     "America/Argentina/San_Luis", | ||||
|     "America/Argentina/Tucuman", | ||||
|     "America/Argentina/Ushuaia", | ||||
|     "America/Aruba", | ||||
|     "America/Asuncion", | ||||
|     "America/Atikokan", | ||||
|     "America/Atka", | ||||
|     "America/Bahia", | ||||
|     "America/Bahia_Banderas", | ||||
|     "America/Barbados", | ||||
|     "America/Belem", | ||||
|     "America/Belize", | ||||
|     "America/Blanc-Sablon", | ||||
|     "America/Boa_Vista", | ||||
|     "America/Bogota", | ||||
|     "America/Boise", | ||||
|     "America/Buenos_Aires", | ||||
|     "America/Cambridge_Bay", | ||||
|     "America/Campo_Grande", | ||||
|     "America/Cancun", | ||||
|     "America/Caracas", | ||||
|     "America/Catamarca", | ||||
|     "America/Cayenne", | ||||
|     "America/Cayman", | ||||
|     "America/Chicago", | ||||
|     "America/Chihuahua", | ||||
|     "America/Ciudad_Juarez", | ||||
|     "America/Coral_Harbour", | ||||
|     "America/Cordoba", | ||||
|     "America/Costa_Rica", | ||||
|     "America/Creston", | ||||
|     "America/Cuiaba", | ||||
|     "America/Curacao", | ||||
|     "America/Danmarkshavn", | ||||
|     "America/Dawson", | ||||
|     "America/Dawson_Creek", | ||||
|     "America/Denver", | ||||
|     "America/Detroit", | ||||
|     "America/Dominica", | ||||
|     "America/Edmonton", | ||||
|     "America/Eirunepe", | ||||
|     "America/El_Salvador", | ||||
|     "America/Ensenada", | ||||
|     "America/Fort_Nelson", | ||||
|     "America/Fort_Wayne", | ||||
|     "America/Fortaleza", | ||||
|     "America/Glace_Bay", | ||||
|     "America/Godthab", | ||||
|     "America/Goose_Bay", | ||||
|     "America/Grand_Turk", | ||||
|     "America/Grenada", | ||||
|     "America/Guadeloupe", | ||||
|     "America/Guatemala", | ||||
|     "America/Guayaquil", | ||||
|     "America/Guyana", | ||||
|     "America/Halifax", | ||||
|     "America/Havana", | ||||
|     "America/Hermosillo", | ||||
|     "America/Indiana/Indianapolis", | ||||
|     "America/Indiana/Knox", | ||||
|     "America/Indiana/Marengo", | ||||
|     "America/Indiana/Petersburg", | ||||
|     "America/Indiana/Tell_City", | ||||
|     "America/Indiana/Vevay", | ||||
|     "America/Indiana/Vincennes", | ||||
|     "America/Indiana/Winamac", | ||||
|     "America/Indianapolis", | ||||
|     "America/Inuvik", | ||||
|     "America/Iqaluit", | ||||
|     "America/Jamaica", | ||||
|     "America/Jujuy", | ||||
|     "America/Juneau", | ||||
|     "America/Kentucky/Louisville", | ||||
|     "America/Kentucky/Monticello", | ||||
|     "America/Knox_IN", | ||||
|     "America/Kralendijk", | ||||
|     "America/La_Paz", | ||||
|     "America/Lima", | ||||
|     "America/Los_Angeles", | ||||
|     "America/Louisville", | ||||
|     "America/Lower_Princes", | ||||
|     "America/Maceio", | ||||
|     "America/Managua", | ||||
|     "America/Manaus", | ||||
|     "America/Marigot", | ||||
|     "America/Martinique", | ||||
|     "America/Matamoros", | ||||
|     "America/Mazatlan", | ||||
|     "America/Mendoza", | ||||
|     "America/Menominee", | ||||
|     "America/Merida", | ||||
|     "America/Metlakatla", | ||||
|     "America/Mexico_City", | ||||
|     "America/Miquelon", | ||||
|     "America/Moncton", | ||||
|     "America/Monterrey", | ||||
|     "America/Montevideo", | ||||
|     "America/Montreal", | ||||
|     "America/Montserrat", | ||||
|     "America/Nassau", | ||||
|     "America/New_York", | ||||
|     "America/Nipigon", | ||||
|     "America/Nome", | ||||
|     "America/Noronha", | ||||
|     "America/North_Dakota/Beulah", | ||||
|     "America/North_Dakota/Center", | ||||
|     "America/North_Dakota/New_Salem", | ||||
|     "America/Nuuk", | ||||
|     "America/Ojinaga", | ||||
|     "America/Panama", | ||||
|     "America/Pangnirtung", | ||||
|     "America/Paramaribo", | ||||
|     "America/Phoenix", | ||||
|     "America/Port-au-Prince", | ||||
|     "America/Port_of_Spain", | ||||
|     "America/Porto_Acre", | ||||
|     "America/Porto_Velho", | ||||
|     "America/Puerto_Rico", | ||||
|     "America/Punta_Arenas", | ||||
|     "America/Rainy_River", | ||||
|     "America/Rankin_Inlet", | ||||
|     "America/Recife", | ||||
|     "America/Regina", | ||||
|     "America/Resolute", | ||||
|     "America/Rio_Branco", | ||||
|     "America/Rosario", | ||||
|     "America/Santa_Isabel", | ||||
|     "America/Santarem", | ||||
|     "America/Santiago", | ||||
|     "America/Santo_Domingo", | ||||
|     "America/Sao_Paulo", | ||||
|     "America/Scoresbysund", | ||||
|     "America/Shiprock", | ||||
|     "America/Sitka", | ||||
|     "America/St_Barthelemy", | ||||
|     "America/St_Johns", | ||||
|     "America/St_Kitts", | ||||
|     "America/St_Lucia", | ||||
|     "America/St_Thomas", | ||||
|     "America/St_Vincent", | ||||
|     "America/Swift_Current", | ||||
|     "America/Tegucigalpa", | ||||
|     "America/Thule", | ||||
|     "America/Thunder_Bay", | ||||
|     "America/Tijuana", | ||||
|     "America/Toronto", | ||||
|     "America/Tortola", | ||||
|     "America/Vancouver", | ||||
|     "America/Virgin", | ||||
|     "America/Whitehorse", | ||||
|     "America/Winnipeg", | ||||
|     "America/Yakutat", | ||||
|     "America/Yellowknife", | ||||
|     "Antarctica/Casey", | ||||
|     "Antarctica/Davis", | ||||
|     "Antarctica/DumontDUrville", | ||||
|     "Antarctica/Macquarie", | ||||
|     "Antarctica/Mawson", | ||||
|     "Antarctica/McMurdo", | ||||
|     "Antarctica/Palmer", | ||||
|     "Antarctica/Rothera", | ||||
|     "Antarctica/South_Pole", | ||||
|     "Antarctica/Syowa", | ||||
|     "Antarctica/Troll", | ||||
|     "Antarctica/Vostok", | ||||
|     "Arctic/Longyearbyen", | ||||
|     "Asia/Aden", | ||||
|     "Asia/Almaty", | ||||
|     "Asia/Amman", | ||||
|     "Asia/Anadyr", | ||||
|     "Asia/Aqtau", | ||||
|     "Asia/Aqtobe", | ||||
|     "Asia/Ashgabat", | ||||
|     "Asia/Ashkhabad", | ||||
|     "Asia/Atyrau", | ||||
|     "Asia/Baghdad", | ||||
|     "Asia/Bahrain", | ||||
|     "Asia/Baku", | ||||
|     "Asia/Bangkok", | ||||
|     "Asia/Barnaul", | ||||
|     "Asia/Beirut", | ||||
|     "Asia/Bishkek", | ||||
|     "Asia/Brunei", | ||||
|     "Asia/Calcutta", | ||||
|     "Asia/Chita", | ||||
|     "Asia/Choibalsan", | ||||
|     "Asia/Chongqing", | ||||
|     "Asia/Chungking", | ||||
|     "Asia/Colombo", | ||||
|     "Asia/Dacca", | ||||
|     "Asia/Damascus", | ||||
|     "Asia/Dhaka", | ||||
|     "Asia/Dili", | ||||
|     "Asia/Dubai", | ||||
|     "Asia/Dushanbe", | ||||
|     "Asia/Famagusta", | ||||
|     "Asia/Gaza", | ||||
|     "Asia/Harbin", | ||||
|     "Asia/Hebron", | ||||
|     "Asia/Ho_Chi_Minh", | ||||
|     "Asia/Hong_Kong", | ||||
|     "Asia/Hovd", | ||||
|     "Asia/Irkutsk", | ||||
|     "Asia/Istanbul", | ||||
|     "Asia/Jakarta", | ||||
|     "Asia/Jayapura", | ||||
|     "Asia/Jerusalem", | ||||
|     "Asia/Kabul", | ||||
|     "Asia/Kamchatka", | ||||
|     "Asia/Karachi", | ||||
|     "Asia/Kashgar", | ||||
|     "Asia/Kathmandu", | ||||
|     "Asia/Katmandu", | ||||
|     "Asia/Khandyga", | ||||
|     "Asia/Kolkata", | ||||
|     "Asia/Krasnoyarsk", | ||||
|     "Asia/Kuala_Lumpur", | ||||
|     "Asia/Kuching", | ||||
|     "Asia/Kuwait", | ||||
|     "Asia/Macao", | ||||
|     "Asia/Macau", | ||||
|     "Asia/Magadan", | ||||
|     "Asia/Makassar", | ||||
|     "Asia/Manila", | ||||
|     "Asia/Muscat", | ||||
|     "Asia/Nicosia", | ||||
|     "Asia/Novokuznetsk", | ||||
|     "Asia/Novosibirsk", | ||||
|     "Asia/Omsk", | ||||
|     "Asia/Oral", | ||||
|     "Asia/Phnom_Penh", | ||||
|     "Asia/Pontianak", | ||||
|     "Asia/Pyongyang", | ||||
|     "Asia/Qatar", | ||||
|     "Asia/Qostanay", | ||||
|     "Asia/Qyzylorda", | ||||
|     "Asia/Rangoon", | ||||
|     "Asia/Riyadh", | ||||
|     "Asia/Saigon", | ||||
|     "Asia/Sakhalin", | ||||
|     "Asia/Samarkand", | ||||
|     "Asia/Seoul", | ||||
|     "Asia/Shanghai", | ||||
|     "Asia/Singapore", | ||||
|     "Asia/Srednekolymsk", | ||||
|     "Asia/Taipei", | ||||
|     "Asia/Tashkent", | ||||
|     "Asia/Tbilisi", | ||||
|     "Asia/Tehran", | ||||
|     "Asia/Tel_Aviv", | ||||
|     "Asia/Thimbu", | ||||
|     "Asia/Thimphu", | ||||
|     "Asia/Tokyo", | ||||
|     "Asia/Tomsk", | ||||
|     "Asia/Ujung_Pandang", | ||||
|     "Asia/Ulaanbaatar", | ||||
|     "Asia/Ulan_Bator", | ||||
|     "Asia/Urumqi", | ||||
|     "Asia/Ust-Nera", | ||||
|     "Asia/Vientiane", | ||||
|     "Asia/Vladivostok", | ||||
|     "Asia/Yakutsk", | ||||
|     "Asia/Yangon", | ||||
|     "Asia/Yekaterinburg", | ||||
|     "Asia/Yerevan", | ||||
|     "Atlantic/Azores", | ||||
|     "Atlantic/Bermuda", | ||||
|     "Atlantic/Canary", | ||||
|     "Atlantic/Cape_Verde", | ||||
|     "Atlantic/Faeroe", | ||||
|     "Atlantic/Faroe", | ||||
|     "Atlantic/Jan_Mayen", | ||||
|     "Atlantic/Madeira", | ||||
|     "Atlantic/Reykjavik", | ||||
|     "Atlantic/South_Georgia", | ||||
|     "Atlantic/St_Helena", | ||||
|     "Atlantic/Stanley", | ||||
|     "Australia/ACT", | ||||
|     "Australia/Adelaide", | ||||
|     "Australia/Brisbane", | ||||
|     "Australia/Broken_Hill", | ||||
|     "Australia/Canberra", | ||||
|     "Australia/Currie", | ||||
|     "Australia/Darwin", | ||||
|     "Australia/Eucla", | ||||
|     "Australia/Hobart", | ||||
|     "Australia/LHI", | ||||
|     "Australia/Lindeman", | ||||
|     "Australia/Lord_Howe", | ||||
|     "Australia/Melbourne", | ||||
|     "Australia/North", | ||||
|     "Australia/NSW", | ||||
|     "Australia/Perth", | ||||
|     "Australia/Queensland", | ||||
|     "Australia/South", | ||||
|     "Australia/Sydney", | ||||
|     "Australia/Tasmania", | ||||
|     "Australia/Victoria", | ||||
|     "Australia/West", | ||||
|     "Australia/Yancowinna", | ||||
|     "Brazil/Acre", | ||||
|     "Brazil/DeNoronha", | ||||
|     "Brazil/East", | ||||
|     "Brazil/West", | ||||
|     "Canada/Atlantic", | ||||
|     "Canada/Central", | ||||
|     "Canada/Eastern", | ||||
|     "Canada/Mountain", | ||||
|     "Canada/Newfoundland", | ||||
|     "Canada/Pacific", | ||||
|     "Canada/Saskatchewan", | ||||
|     "Canada/Yukon", | ||||
|     "CET", | ||||
|     "Chile/Continental", | ||||
|     "Chile/EasterIsland", | ||||
|     "CST6CDT", | ||||
|     "Cuba", | ||||
|     "EET", | ||||
|     "Egypt", | ||||
|     "Eire", | ||||
|     "EST", | ||||
|     "EST5EDT", | ||||
|     "Etc/GMT", | ||||
|     "Etc/GMT+0", | ||||
|     "Etc/GMT+1", | ||||
|     "Etc/GMT+10", | ||||
|     "Etc/GMT+11", | ||||
|     "Etc/GMT+12", | ||||
|     "Etc/GMT+2", | ||||
|     "Etc/GMT+3", | ||||
|     "Etc/GMT+4", | ||||
|     "Etc/GMT+5", | ||||
|     "Etc/GMT+6", | ||||
|     "Etc/GMT+7", | ||||
|     "Etc/GMT+8", | ||||
|     "Etc/GMT+9", | ||||
|     "Etc/GMT-0", | ||||
|     "Etc/GMT-1", | ||||
|     "Etc/GMT-10", | ||||
|     "Etc/GMT-11", | ||||
|     "Etc/GMT-12", | ||||
|     "Etc/GMT-13", | ||||
|     "Etc/GMT-14", | ||||
|     "Etc/GMT-2", | ||||
|     "Etc/GMT-3", | ||||
|     "Etc/GMT-4", | ||||
|     "Etc/GMT-5", | ||||
|     "Etc/GMT-6", | ||||
|     "Etc/GMT-7", | ||||
|     "Etc/GMT-8", | ||||
|     "Etc/GMT-9", | ||||
|     "Etc/GMT0", | ||||
|     "Etc/Greenwich", | ||||
|     "Etc/UCT", | ||||
|     "Etc/Universal", | ||||
|     "Etc/UTC", | ||||
|     "Etc/Zulu", | ||||
|     "Europe/Amsterdam", | ||||
|     "Europe/Andorra", | ||||
|     "Europe/Astrakhan", | ||||
|     "Europe/Athens", | ||||
|     "Europe/Belfast", | ||||
|     "Europe/Belgrade", | ||||
|     "Europe/Berlin", | ||||
|     "Europe/Bratislava", | ||||
|     "Europe/Brussels", | ||||
|     "Europe/Bucharest", | ||||
|     "Europe/Budapest", | ||||
|     "Europe/Busingen", | ||||
|     "Europe/Chisinau", | ||||
|     "Europe/Copenhagen", | ||||
|     "Europe/Dublin", | ||||
|     "Europe/Gibraltar", | ||||
|     "Europe/Guernsey", | ||||
|     "Europe/Helsinki", | ||||
|     "Europe/Isle_of_Man", | ||||
|     "Europe/Istanbul", | ||||
|     "Europe/Jersey", | ||||
|     "Europe/Kaliningrad", | ||||
|     "Europe/Kiev", | ||||
|     "Europe/Kirov", | ||||
|     "Europe/Kyiv", | ||||
|     "Europe/Lisbon", | ||||
|     "Europe/Ljubljana", | ||||
|     "Europe/London", | ||||
|     "Europe/Luxembourg", | ||||
|     "Europe/Madrid", | ||||
|     "Europe/Malta", | ||||
|     "Europe/Mariehamn", | ||||
|     "Europe/Minsk", | ||||
|     "Europe/Monaco", | ||||
|     "Europe/Moscow", | ||||
|     "Europe/Nicosia", | ||||
|     "Europe/Oslo", | ||||
|     "Europe/Paris", | ||||
|     "Europe/Podgorica", | ||||
|     "Europe/Prague", | ||||
|     "Europe/Riga", | ||||
|     "Europe/Rome", | ||||
|     "Europe/Samara", | ||||
|     "Europe/San_Marino", | ||||
|     "Europe/Sarajevo", | ||||
|     "Europe/Saratov", | ||||
|     "Europe/Simferopol", | ||||
|     "Europe/Skopje", | ||||
|     "Europe/Sofia", | ||||
|     "Europe/Stockholm", | ||||
|     "Europe/Tallinn", | ||||
|     "Europe/Tirane", | ||||
|     "Europe/Tiraspol", | ||||
|     "Europe/Ulyanovsk", | ||||
|     "Europe/Uzhgorod", | ||||
|     "Europe/Vaduz", | ||||
|     "Europe/Vatican", | ||||
|     "Europe/Vienna", | ||||
|     "Europe/Vilnius", | ||||
|     "Europe/Volgograd", | ||||
|     "Europe/Warsaw", | ||||
|     "Europe/Zagreb", | ||||
|     "Europe/Zaporozhye", | ||||
|     "Europe/Zurich", | ||||
|     "Factory", | ||||
|     "GB", | ||||
|     "GB-Eire", | ||||
|     "GMT", | ||||
|     "GMT+0", | ||||
|     "GMT-0", | ||||
|     "GMT0", | ||||
|     "Greenwich", | ||||
|     "Hongkong", | ||||
|     "HST", | ||||
|     "Iceland", | ||||
|     "Indian/Antananarivo", | ||||
|     "Indian/Chagos", | ||||
|     "Indian/Christmas", | ||||
|     "Indian/Cocos", | ||||
|     "Indian/Comoro", | ||||
|     "Indian/Kerguelen", | ||||
|     "Indian/Mahe", | ||||
|     "Indian/Maldives", | ||||
|     "Indian/Mauritius", | ||||
|     "Indian/Mayotte", | ||||
|     "Indian/Reunion", | ||||
|     "Iran", | ||||
|     "Israel", | ||||
|     "Jamaica", | ||||
|     "Japan", | ||||
|     "Kwajalein", | ||||
|     "Libya", | ||||
|     "MET", | ||||
|     "Mexico/BajaNorte", | ||||
|     "Mexico/BajaSur", | ||||
|     "Mexico/General", | ||||
|     "MST", | ||||
|     "MST7MDT", | ||||
|     "Navajo", | ||||
|     "NZ", | ||||
|     "NZ-CHAT", | ||||
|     "Pacific/Apia", | ||||
|     "Pacific/Auckland", | ||||
|     "Pacific/Bougainville", | ||||
|     "Pacific/Chatham", | ||||
|     "Pacific/Chuuk", | ||||
|     "Pacific/Easter", | ||||
|     "Pacific/Efate", | ||||
|     "Pacific/Enderbury", | ||||
|     "Pacific/Fakaofo", | ||||
|     "Pacific/Fiji", | ||||
|     "Pacific/Funafuti", | ||||
|     "Pacific/Galapagos", | ||||
|     "Pacific/Gambier", | ||||
|     "Pacific/Guadalcanal", | ||||
|     "Pacific/Guam", | ||||
|     "Pacific/Honolulu", | ||||
|     "Pacific/Johnston", | ||||
|     "Pacific/Kanton", | ||||
|     "Pacific/Kiritimati", | ||||
|     "Pacific/Kosrae", | ||||
|     "Pacific/Kwajalein", | ||||
|     "Pacific/Majuro", | ||||
|     "Pacific/Marquesas", | ||||
|     "Pacific/Midway", | ||||
|     "Pacific/Nauru", | ||||
|     "Pacific/Niue", | ||||
|     "Pacific/Norfolk", | ||||
|     "Pacific/Noumea", | ||||
|     "Pacific/Pago_Pago", | ||||
|     "Pacific/Palau", | ||||
|     "Pacific/Pitcairn", | ||||
|     "Pacific/Pohnpei", | ||||
|     "Pacific/Ponape", | ||||
|     "Pacific/Port_Moresby", | ||||
|     "Pacific/Rarotonga", | ||||
|     "Pacific/Saipan", | ||||
|     "Pacific/Samoa", | ||||
|     "Pacific/Tahiti", | ||||
|     "Pacific/Tarawa", | ||||
|     "Pacific/Tongatapu", | ||||
|     "Pacific/Truk", | ||||
|     "Pacific/Wake", | ||||
|     "Pacific/Wallis", | ||||
|     "Pacific/Yap", | ||||
|     "Poland", | ||||
|     "Portugal", | ||||
|     "PRC", | ||||
|     "PST8PDT", | ||||
|     "ROC", | ||||
|     "ROK", | ||||
|     "Singapore", | ||||
|     "Turkey", | ||||
|     "UCT", | ||||
|     "Universal", | ||||
|     "US/Alaska", | ||||
|     "US/Aleutian", | ||||
|     "US/Arizona", | ||||
|     "US/Central", | ||||
|     "US/East-Indiana", | ||||
|     "US/Eastern", | ||||
|     "US/Hawaii", | ||||
|     "US/Indiana-Starke", | ||||
|     "US/Michigan", | ||||
|     "US/Mountain", | ||||
|     "US/Pacific", | ||||
|     "US/Samoa", | ||||
|     "UTC", | ||||
|     "W-SU", | ||||
|     "WET", | ||||
|     "Zulu", | ||||
| ]; | ||||
| 
 | ||||
| export default TIME_ZONES; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user