diff --git a/package.json b/package.json index 81354d2a..69de231c 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,12 @@ "@emotion/styled": "^11.3.0", "@haleos/ra-language-german": "^1.0.0", "@haxqer/ra-language-chinese": "^4.16.2", + "@matrix-org/spec": "^1.10.1", "@mui/icons-material": "^5.15.16", "@mui/material": "^5.16.0", "history": "^5.1.0", "lodash": "^4.17.21", + "openapi-fetch": "^0.9.5", "papaparse": "^5.4.1", "query-string": "^7.1.1", "ra-core": "^4.16.17", @@ -91,8 +93,8 @@ ], "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/stylistic", + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", "plugin:import/typescript", "plugin:yaml/recommended" ], diff --git a/src/App.tsx b/src/App.tsx index 698c66f7..e3c2f40b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin import { Route } from "react-router-dom"; import { ImportFeature } from "./components/ImportFeature"; +import { SynapseTranslationMessages } from "./i18n"; import germanMessages from "./i18n/de"; import englishMessages from "./i18n/en"; import frenchMessages from "./i18n/fr"; @@ -30,7 +31,7 @@ const messages = { zh: chineseMessages, }; const i18nProvider = polyglotI18nProvider( - locale => (messages[locale] ? merge({}, messages.en, messages[locale]) : messages.en), + locale => (messages[locale] ? (merge({}, messages.en, messages[locale]) as SynapseTranslationMessages) : messages.en), resolveBrowserLocale(), [ { locale: "en", name: "English" }, diff --git a/src/components/AvatarField.tsx b/src/components/AvatarField.tsx index 0a8e2328..e4edbdd2 100644 --- a/src/components/AvatarField.tsx +++ b/src/components/AvatarField.tsx @@ -1,11 +1,11 @@ import { get } from "lodash"; -import { Avatar } from "@mui/material"; +import { Avatar, AvatarProps } from "@mui/material"; import { useRecordContext } from "react-admin"; -const AvatarField = ({ source, ...rest }) => { +const AvatarField = ({ source, ...rest }: AvatarProps & { source: string }) => { const record = useRecordContext(rest); - const src = get(record, source)?.toString(); + const src = get(record, source, "") as string; const { alt, classes, sizes, sx, variant } = rest; return ; }; diff --git a/src/components/ImportFeature.tsx b/src/components/ImportFeature.tsx index 062af3da..13552db3 100644 --- a/src/components/ImportFeature.tsx +++ b/src/components/ImportFeature.tsx @@ -21,7 +21,7 @@ const LOGGING = true; const expectedFields = ["id", "displayname"].sort(); -function TranslatableOption({ value, text }) { +function TranslatableOption({ value, text }: { value: string; text: string }) { const translate = useTranslate(); return ; } @@ -81,7 +81,7 @@ const FilePicker = () => { const dataProvider = useDataProvider(); - const onFileChange = async (e: ChangeEvent) => { + const onFileChange = (e: ChangeEvent) => { if (progress !== null) return; setValues([]); @@ -106,11 +106,11 @@ const FilePicker = () => { skipEmptyLines: true /* especially for a final EOL in the csv file */, complete: result => { if (result.errors) { - setError(result.errors.map(e => e.toString())); + setError(result.errors.map(e => String(e))); } /* Papaparse is very lenient, we may be able to salvage * the data in the file. */ - verifyCsv(result, { setValues, setStats, setError }); + verifyCsv(result, setValues, setStats, setError); }, }); } catch { @@ -119,7 +119,12 @@ const FilePicker = () => { } }; - const verifyCsv = ({ data, meta, errors }: ParseResult, { setValues, setStats, setError }) => { + const verifyCsv = ( + { data, meta, errors }: ParseResult, + setValues: (values: ImportLine[]) => void, + setStats: (stats: ChangeStats | null) => void, + setError: (error: string | string[] | null) => void + ) => { /* First, verify the presence of required fields */ const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF)); @@ -206,29 +211,23 @@ const FilePicker = () => { return true; }; - const runImport = async () => { + const runImport = () => { if (progress !== null) { notify("import_users.errors.already_in_progress"); return; } - const results = await doImport( - dataProvider, - values, - conflictMode, - passwordMode, - useridMode, - dryRun, - setProgress, - setError + void doImport(dataProvider, values, conflictMode, passwordMode, useridMode, dryRun, setProgress, setError).then( + results => { + setImportResults(results); + // offer CSV download of skipped or errored records + // (so that the user doesn't have to filter out successful + // records manually when fixing stuff in the CSV) + setSkippedRecords(unparseCsv(results.skippedRecords)); + if (LOGGING) console.log("Skipped records:"); + if (LOGGING) console.log(skippedRecords); + } ); - setImportResults(results); - // offer CSV download of skipped or errored records - // (so that the user doesn't have to filter out successful - // records manually when fixing stuff in the CSV) - setSkippedRecords(unparseCsv(results.skippedRecords)); - if (LOGGING) console.log("Skipped records:"); - if (LOGGING) console.log(skippedRecords); }; // XXX every single one of the requests will restart the activity indicator @@ -370,7 +369,7 @@ const FilePicker = () => { element.click(); }; - const onConflictModeChanged = async (e: ChangeEvent) => { + const onConflictModeChanged = (e: ChangeEvent) => { if (progress !== null) { return; } @@ -387,7 +386,7 @@ const FilePicker = () => { setPasswordMode(e.target.checked); }; - const onUseridModeChanged = async (e: ChangeEvent) => { + const onUseridModeChanged = (e: ChangeEvent) => { if (progress !== null) { return; } diff --git a/src/components/ServerNotices.tsx b/src/components/ServerNotices.tsx index f269a7fb..5fcd87d9 100644 --- a/src/components/ServerNotices.tsx +++ b/src/components/ServerNotices.tsx @@ -40,6 +40,7 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => { {translate("resources.servernotices.helper.send")} } onSubmit={onSubmit}> + {/* TODO: Use MUI form (does not require a record) */} { redirect={false} translateOptions={{ id: record.id, - name: record.display_name ? record.display_name : record.id, + name: String(record.display_name ?? record.id), }} /> ); diff --git a/src/components/media.tsx b/src/components/media.tsx index 893b1af7..39c4299c 100644 --- a/src/components/media.tsx +++ b/src/components/media.tsx @@ -35,7 +35,13 @@ import { dateParser } from "./date"; import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider"; import { getMediaUrl } from "../synapse/synapse"; -const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { +interface DeleteMediaDialogProps { + open: boolean; + onClose: () => void; + onSubmit: (params: DeleteMediaParams) => void; +} + +const DeleteMediaDialog = ({ open, onClose, onSubmit }: DeleteMediaDialogProps) => { const translate = useTranslate(); const DeleteMediaToolbar = (props: ToolbarProps) => ( @@ -53,6 +59,7 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { {translate("delete_media.helper.send")} } onSubmit={onSubmit}> + {/* TODO: Use MUI form (does not require a record) */} ({ display: "flex", @@ -115,20 +114,14 @@ const LoginPage = () => { console.log("Base URL is:", baseUrl); console.log("SSO Token is:", ssoToken); console.log("Let's try token login..."); - login(auth).catch(error => { - alert( - typeof error === "string" - ? error - : typeof error === "undefined" || !error.message - ? "ra.auth.sign_in_error" - : error.message - ); + login(auth).catch((error: Error | string | undefined) => { + alert(typeof error === "string" ? error : !error?.message ? "ra.auth.sign_in_error" : error.message); console.error(error); }); } } - const validateBaseUrl = value => { + const validateBaseUrl = (value: string) => { if (!value.match(/^(http|https):\/\//)) { return translate("synapseadmin.auth.protocol_error"); } else if (!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)) { @@ -140,16 +133,11 @@ const LoginPage = () => { const handleSubmit = auth => { setLoading(true); - login(auth).catch(error => { + login(auth).catch((error: Error | string | undefined) => { setLoading(false); - notify( - typeof error === "string" - ? error - : typeof error === "undefined" || !error.message - ? "ra.auth.sign_in_error" - : error.message, - { type: "warning" } - ); + notify(typeof error === "string" ? error : !error?.message ? "ra.auth.sign_in_error" : error.message, { + type: "warning", + }); }); }; @@ -161,7 +149,7 @@ const LoginPage = () => { window.location.href = ssoFullUrl; }; - const UserData = ({ formData }) => { + const UserData = ({ formData }: { formData: UserDataFields }) => { const form = useFormContext(); const [serverVersion, setServerVersion] = useState(""); const [matrixVersions, setMatrixVersions] = useState(""); @@ -171,7 +159,7 @@ const LoginPage = () => { // check if username is a full qualified userId then set base_url accordingly const domain = splitMxid(formData.username)?.domain; if (domain) { - getWellKnownUrl(domain).then(url => { + void getWellKnownUrl(domain).then(url => { if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url))) form.setValue("base_url", url); }); @@ -183,22 +171,25 @@ const LoginPage = () => { form.setValue("base_url", restrictBaseUrl[0]); } if (!isValidBaseUrl(formData.base_url)) return; + const synapseClient = useSynapse(formData.base_url); getServerVersion(formData.base_url) .then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`)) .catch(() => setServerVersion("")); - getSupportedFeatures(formData.base_url) + synapseClient + .getSupportedFeatures() .then(features => - setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`) + setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features?.versions.join(", ")}`) ) .catch(() => setMatrixVersions("")); // Set SSO Url - getSupportedLoginFlows(formData.base_url) + synapseClient + .getSupportedLoginFlows() .then(loginFlows => { - const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined; - const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined; + const supportPass = loginFlows?.find(f => f.type === "m.login.password") !== undefined; + const supportSSO = loginFlows?.find(f => f.type === "m.login.sso") !== undefined; setSupportPassAuth(supportPass); setSSOBaseUrl(supportSSO ? formData.base_url : ""); }) @@ -286,7 +277,7 @@ const LoginPage = () => { ))} - {formDataProps => } + >{formDataProps => }