diff --git a/src/App.js b/src/App.js index 4bf5fc5..b64587e 100644 --- a/src/App.js +++ b/src/App.js @@ -40,6 +40,7 @@ const App = () => ( dataProvider={dataProvider} i18nProvider={i18nProvider} customRoutes={[ + , , ]} > diff --git a/src/components/ImportFeature.js b/src/components/ImportFeature.js index 565855d..6ba745a 100644 --- a/src/components/ImportFeature.js +++ b/src/components/ImportFeature.js @@ -20,6 +20,7 @@ import { import { useTranslate } from "ra-core"; import Container from "@material-ui/core/Container/Container"; import { generateRandomUser } from "./users"; +import ShowUserPdf from "./ShowUserPdf"; const LOGGING = true; @@ -59,6 +60,8 @@ const FilePicker = props => { const [progress, setProgress] = useState(null); + const [pdfRecords, setPdfRecords] = useState(null); + const [importResults, setImportResults] = useState(null); const [skippedRecords, setSkippedRecords] = useState(null); @@ -66,17 +69,23 @@ const FilePicker = props => { const [passwordMode, setPasswordMode] = useState(true); const [useridMode, setUseridMode] = useState("ignore"); + const [showingPdf, setShowingPdf] = useState(false); + const translate = useTranslate(); const notify = useNotify(); const dataProvider = useDataProvider(); const onFileChange = async e => { - if (progress !== null) return; - + if (progress !== null) { + return; + } + if (LOGGING) console.log("onFileChange was called"); setValues(null); setError(null); setStats(null); + setPdfRecords(null); + setImportResults(null); const file = e.target.files ? e.target.files[0] : null; /* Let's refuse some unreasonably big files instead of freezing @@ -126,6 +135,11 @@ const FilePicker = props => { }); if (eF.length !== 0) { + if (LOGGING) { + console.log(meta.fields); + console.log(eF); + console.log(oF); + } setError( translate("import_users.error.required_field", { field: eF[0] }) ); @@ -226,6 +240,9 @@ const FilePicker = props => { setProgress, setError ); + + setPdfRecords(results.recordsForPdf); + setImportResults(results); // offer CSV download of skipped or errored records // (so that the user doesn't have to filter out successful @@ -251,6 +268,8 @@ const FilePicker = props => { let skippedRecords = []; let erroredRecords = []; let succeededRecords = []; + let recordsForPdf = []; + let changeStats = { toAdmin: 0, toGuest: 0, @@ -365,6 +384,14 @@ const FilePicker = props => { await dataProvider.create("users", { data: recordData }); } succeededRecords.push(recordData); + + if (recordData.password !== undefined) { + recordsForPdf.push({ + id: recordData.id, + password: recordData.password, + displayname: recordData.displayname, + }); + } } ); }; @@ -389,6 +416,7 @@ const FilePicker = props => { erroredRecords, succeededRecords, totalRecordCount: entriesCount, + recordsForPdf, changeStats, wasDryRun: dryRun, }; @@ -618,6 +646,10 @@ const FilePicker = props => {
, ] : ""} + {translate( + "import_users.cards.results.for_print", + importResults.recordsForPdf.length + )}
{importResults.wasDryRun && [ translate("import_users.cards.results.simulated_only"), @@ -655,20 +687,43 @@ const FilePicker = props => { ); - let allCards = []; - if (uploadCard) allCards.push(uploadCard); - if (errorCards) allCards.push(errorCards); - if (conflictCards) allCards.push(conflictCards); - if (statsCards) allCards.push(...statsCards); - if (startImportCard) allCards.push(startImportCard); - if (resultsCard) allCards.push(resultsCard); + let pdfDisplay = + pdfRecords && showingPdf && pdfRecords.length ? ( + + ) : null; - let cardContainer = {allCards}; + let pdfActions = pdfRecords ? ( + + + + ) : null; - return [ - , - cardContainer, - ]; + if (pdfRecords && showingPdf) { + return <Card>{pdfDisplay}</Card>; + } else { + let allCards = []; + if (uploadCard) allCards.push(uploadCard); + if (errorCards) allCards.push(errorCards); + if (conflictCards) allCards.push(conflictCards); + if (statsCards) allCards.push(...statsCards); + if (startImportCard) allCards.push(startImportCard); + if (resultsCard) allCards.push(resultsCard); + if (pdfActions) allCards.push(pdfActions); + + let cardContainer = <Card>{allCards}</Card>; + + return [ + <Title defaultTitle={translate("import_users.title")} />, + cardContainer, + ]; + } }; export const ImportFeature = FilePicker; diff --git a/src/components/ShowUserPdf.js b/src/components/ShowUserPdf.js index ffcc5b6..cb380f1 100644 --- a/src/components/ShowUserPdf.js +++ b/src/components/ShowUserPdf.js @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useRef } from "react"; import { Title, Button } from "react-admin"; import { makeStyles } from "@material-ui/core/styles"; import { PDFExport } from "@progress/kendo-react-pdf"; import QRCode from "qrcode.react"; +import { string, any } from "prop-types"; function xor(a, b) { var res = ""; @@ -14,109 +15,204 @@ function xor(a, b) { function calculateQrString(serverUrl, username, password) { const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7"; - var urlString = "user=" + username + "&password=" + password; + const origUrlString = "user=" + username + "&password=" + password; - urlString = xor(urlString, magicString); // xor with magic string + var urlString = xor(origUrlString, magicString); // xor with magic string + if (origUrlString !== xor(urlString, magicString)) { + console.error( + "xoring this url string with magicString twice gave different results:", + origUrlString, + urlString, + xor(urlString, magicString) + ); + } urlString = btoa(urlString); // to base64 return serverUrl + "/#" + urlString; } +UserPdfPage.propTypes = { + classes: any, + displayname: string, + qrCode: any, + serverUrl: string, + username: string, + password: string, +}; + +function UserPdfPage({ + classes, + displayname, + qrCode, + serverUrl, + username, + password, +}) { + return ( + <div className={classes.page}> + <div className={classes.header}> + <div className={classes.name}>{displayname}</div> + <img className={classes.logo} alt="Logo" src="images/logo.png" /> + </div> + <div className={classes.body}> + <table> + <tbody> + <tr> + <td width="200px"> + <div className={classes.code_note}> + Ihr persönlicher Anmeldecode: + </div> + </td> + <td className={classes.table_cell}> + <div className={classes.credentials_note}> + Ihre persönlichen Zugangsdaten: + </div> + </td> + </tr> + <tr> + <td> + <div className={classes.qr}>{qrCode}</div> + </td> + <td className={classes.table_cell}> + <div className={classes.credentials_text}> + <br /> + <table> + <tbody> + <tr> + <td>Heimserver:</td> + <td> + <span className={classes.credentials}> + {serverUrl} + </span> + </td> + </tr> + <tr> + <td>Benutzername:</td> + <td> + <span className={classes.credentials}> + {username} + </span> + </td> + </tr> + <tr> + <td>Passwort:</td> + <td> + <span className={classes.credentials}> + {password} + </span> + </td> + </tr> + </tbody> + </table> + </div> + </td> + </tr> + </tbody> + </table> + <div className={classes.note}> + Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase + notieren: + <br /> + <br /> + <br /> + <hr /> + </div> + </div> + </div> + ); +} + +const useStyles = makeStyles(theme => ({ + page: { + height: 800, + width: 566, + padding: "none", + backgroundColor: "white", + boxShadow: "5px 5px 5px black", + margin: "auto", + overflowX: "hidden", + overflowY: "hidden", + fontFamily: "DejaVu Sans, Sans-Serif", + fontSize: 15, + }, + header: { + height: 144, + width: 534, + marginLeft: 32, + marginTop: 15, + }, + name: { + width: 240, + fontSize: 35, + float: "left", + marginTop: 100, + }, + logo: { + width: 90, + marginTop: 50, + marginRight: 70, + float: "right", + }, + body: { + clear: "both", + }, + table_cell: { + verticalAlign: "top", + }, + code_note: { + marginLeft: 32, + marginTop: 86, + }, + qr: { + marginTop: 15, + marginLeft: 32, + }, + credentials_note: { + marginTop: 86, + marginLeft: 10, + }, + credentials_text: { + marginLeft: 10, + fontSize: 12, + }, + credentials: { + fontFamily: "DejaVu Sans Mono, monospace", + }, + note: { + fontSize: 18, + marginTop: 100, + marginLeft: 32, + marginRight: 32, + }, +})); + const ShowUserPdf = props => { - const useStyles = makeStyles(theme => ({ - page: { - height: 800, - width: 566, - padding: "none", - backgroundColor: "white", - boxShadow: "5px 5px 5px black", - margin: "auto", - overflowX: "hidden", - overflowY: "hidden", - fontFamily: "DejaVu Sans, Sans-Serif", - fontSize: 15, - }, - header: { - height: 144, - width: 534, - marginLeft: 32, - marginTop: 15, - }, - name: { - width: 240, - fontSize: 35, - float: "left", - marginTop: 100, - }, - logo: { - width: 90, - marginTop: 50, - marginRight: 70, - float: "right", - }, - body: { - clear: "both", - }, - table_cell: { - verticalAlign: "top", - }, - code_note: { - marginLeft: 32, - marginTop: 86, - }, - qr: { - marginTop: 15, - marginLeft: 32, - }, - credentials_note: { - marginTop: 86, - marginLeft: 10, - }, - credentials_text: { - marginLeft: 10, - fontSize: 12, - }, - credentials: { - fontFamily: "DejaVu Sans Mono, monospace", - }, - note: { - fontSize: 18, - marginTop: 100, - marginLeft: 32, - marginRight: 32, - }, - })); - const classes = useStyles(); - - var resume; + const userPdf = useRef(null); const exportPDF = () => { - resume.save(); + userPdf.current.save(); }; - var qrCode = ""; - var displayname = ""; - var id = ""; - var password = ""; - var username = ""; - var serverUrl = ""; + let userRecords; + + if (props.records) { + userRecords = props.records; + } if ( + props.location && props.location.state && props.location.state.id && props.location.state.password ) { - id = props.location.state.id; - password = props.location.state.password; - - username = id.substring(1, id.indexOf(":")); - serverUrl = "https://" + id.substring(id.indexOf(":") + 1); - - const qrString = calculateQrString(serverUrl, username, password); - - qrCode = <QRCode value={qrString} size={128} />; - displayname = props.location.state.displayname; + userRecords = [ + { + id: props.location.state.id, + password: props.location.state.password, + displayname: props.location.state.displayname, + }, + ]; } return ( @@ -126,82 +222,39 @@ const ShowUserPdf = props => { <PDFExport paperSize={"A4"} - fileName="User.pdf" + fileName="Users.pdf" title="" subject="" keywords="" - ref={r => (resume = r)} + ref={userPdf} + //ref={r => (resume = r)} > - <div className={classes.page}> - <div className={classes.header}> - <div className={classes.name}>{displayname}</div> - <img className={classes.logo} alt="Logo" src="images/logo.png" /> - </div> - <div className={classes.body}> - <table> - <tbody> - <tr> - <td width="200px"> - <div className={classes.code_note}> - Ihr persönlicher Anmeldecode: - </div> - </td> - <td className={classes.table_cell}> - <div className={classes.credentials_note}> - Ihre persönlichen Zugangsdaten: - </div> - </td> - </tr> - <tr> - <td> - <div className={classes.qr}>{qrCode}</div> - </td> - <td className={classes.table_cell}> - <div className={classes.credentials_text}> - <br /> - <table> - <tbody> - <tr> - <td>Heimserver:</td> - <td> - <span className={classes.credentials}> - {serverUrl} - </span> - </td> - </tr> - <tr> - <td>Benutzername:</td> - <td> - <span className={classes.credentials}> - {username} - </span> - </td> - </tr> - <tr> - <td>Passwort:</td> - <td> - <span className={classes.credentials}> - {password} - </span> - </td> - </tr> - </tbody> - </table> - </div> - </td> - </tr> - </tbody> - </table> - <div className={classes.note}> - Hier können Sie Ihre selbst gewählte - Schlüsselsicherungs-Passphrase notieren: - <br /> - <br /> - <br /> - <hr /> - </div> - </div> - </div> + {userRecords.map(record => { + if (record.id && record.password) { + const username = record.id.substring(1, record.id.indexOf(":")); + const serverUrl = + "https://" + record.id.substring(record.id.indexOf(":") + 1); + const qrString = calculateQrString( + serverUrl, + username, + record.password + ); + const qrCode = <QRCode value={qrString} size={128} />; + return ( + <UserPdfPage + classes={classes} + displayname={record.displayname} + qrCode={qrCode} + serverUrl={serverUrl} + username={username} + password={record.password} + /> + ); + } else { + /* Skip empty PDF pages */ + return null; + } + })} </PDFExport> </div> ); diff --git a/src/components/users.js b/src/components/users.js index efefd09..fe7a84a 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -12,10 +12,12 @@ import { ArrayInput, ArrayField, Button, + CreateButton, Datagrid, DateField, Create, Edit, + ExportButton, List, Filter, Toolbar, @@ -37,11 +39,8 @@ import { DeleteButton, SaveButton, regex, - useRedirect, useTranslate, Pagination, - CreateButton, - ExportButton, TopToolbar, sanitizeListRestProps, NumberField, @@ -50,6 +49,13 @@ import SaveQrButton from "./SaveQrButton"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { DeviceRemoveButton } from "./devices"; import { makeStyles } from "@material-ui/core/styles"; +import { Link } from "react-router-dom"; + +const redirect = (basePath, id, data) => { + return { + pathname: "/importcsv", + }; +}; const useStyles = makeStyles({ small: { @@ -81,7 +87,6 @@ const UserListActions = ({ total, ...rest }) => { - const redirectTo = useRedirect(); return ( <TopToolbar className={className} {...sanitizeListRestProps(rest)}> {filters && @@ -101,6 +106,10 @@ const UserListActions = ({ exporter={exporter} maxResults={maxResults} /> + {/* Add your custom actions */} + <Button component={Link} to={redirect} label="CSV Import"> + <GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} /> + </Button> </TopToolbar> ); }; @@ -178,7 +187,7 @@ export const UserList = props => { }; // redirect to the related Author show page -const redirect = (basePath, id, data) => { +const redirectToPdf = (basePath, id, data) => { return { pathname: "/showpdf", state: { @@ -193,7 +202,7 @@ const UserCreateToolbar = props => ( <Toolbar {...props}> <SaveQrButton label="synapseadmin.action.save_and_show" - redirect={redirect} + redirect={redirectToPdf} submitOnEnter={true} /> <SaveButton diff --git a/src/i18n/de.js b/src/i18n/de.js index 37ed757..e5293fb 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -104,6 +104,8 @@ export default { with_error: "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern", simulated_only: "Import-Vorgang war nur simuliert", + for_print: + "%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar", }, }, }, diff --git a/src/i18n/en.js b/src/i18n/en.js index 8e4ea3d..d4cb316 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -104,6 +104,8 @@ export default { with_error: "%{smart_count} entry with errors ||| %{smart_count} entries with errors", simulated_only: "Run was only simulated", + for_print: + "%{smart_count} entry available for printing |||| %{smart_count} entries available for printing", }, }, }, diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index df3a476..df0af3c 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -25,9 +25,6 @@ const mxcUrlToHttp = mxcUrl => { return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; }; -const powerLevelToRole = powerLevel => - powerLevel < 100 ? (powerLevel < 50 ? "user" : "mod") : "admin"; - const POWER_LEVELS = { admin: 100, mod: 50,