CSV import/export, UI/UX improvements to import-export interface

This commit is contained in:
f0x 2023-01-26 18:37:11 +01:00
parent 3960327a43
commit c80786014c
6 changed files with 165 additions and 23 deletions

View file

@ -19,6 +19,7 @@
"langs": "^2.0.0", "langs": "^2.0.0",
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",
"modern-normalize": "^1.1.0", "modern-normalize": "^1.1.0",
"papaparse": "^5.3.2",
"photoswipe": "^5.3.3", "photoswipe": "^5.3.3",
"photoswipe-dynamic-caption-plugin": "^1.2.7", "photoswipe-dynamic-caption-plugin": "^1.2.7",
"photoswipe-video-plugin": "^1.0.2", "photoswipe-video-plugin": "^1.0.2",

View file

@ -0,0 +1,64 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const React = require("react");
module.exports = function ExportFormatTable() {
return (
<table className="export-format-table">
<thead>
<tr>
<th rowSpan={2} />
<th colSpan={2}>Includes</th>
<th colSpan={2}>Importable by</th>
</tr>
<tr>
<th>Domain</th>
<th>Public comment</th>
<th>GoToSocial</th>
<th>Mastodon</th>
</tr>
</thead>
<tbody>
<Format name="Text" info={[true, false, true, false]} />
<Format name="JSON" info={[true, true, true, false]} />
<Format name="CSV" info={[true, true, true, true]} />
</tbody>
</table>
);
};
function Format({ name, info }) {
return (
<tr>
<td><b>{name}</b></td>
{info.map((b, key) => <td key={key} className="bool">{bool(b)}</td>)}
</tr>
);
}
function bool(val) {
return (
<>
<i className={`fa fa-${val ? "check" : "times"}`} aria-hidden="true"></i>
<span className="sr-only">{val ? "Yes" : "No"}</span>
</>
);
}

View file

@ -45,6 +45,7 @@ const MutationButton = require("../../components/form/mutation-button");
const isValidDomain = require("is-valid-domain"); const isValidDomain = require("is-valid-domain");
const FormWithData = require("../../lib/form/form-with-data"); const FormWithData = require("../../lib/form/form-with-data");
const { Error } = require("../../components/error"); const { Error } = require("../../components/error");
const ExportFormatTable = require("./export-format-table");
const baseUrl = "/settings/admin/federation/import-export"; const baseUrl = "/settings/admin/federation/import-export";
@ -104,39 +105,55 @@ module.exports = function ImportExport() {
<Route> <Route>
{parseResult.isSuccess && <Redirect to={`${baseUrl}/list`} />} {parseResult.isSuccess && <Redirect to={`${baseUrl}/list`} />}
<h2>Import / Export suspended domains</h2> <h2>Import / Export suspended domains</h2>
<div> <div>
<form onSubmit={submitParse}> <p>
<TextArea This page can be used to import and export lists of domains to suspend.
field={form.domains} Exports can be done in various formats, with varying functionality and support in other software.
label="Domains, one per line (plaintext) or JSON" Imports will automatically detect what format is being processed.
placeholder={`google.com\nfacebook.com`} </p>
rows={8} <ExportFormatTable />
/> </div>
<div className="import-export">
<TextArea
field={form.domains}
label="Domains"
placeholder={`google.com\nfacebook.com`}
rows={8}
/>
<div className="row"> <div className="row">
<MutationButton label="Import" result={parseResult} showError={false} /> <MutationButton label="Import" type="button" onClick={() => submitParse()} result={parseResult} showError={false} />
<button type="button" className="with-padding"> <MutationButton label="Export" type="button" onClick={() => submitExport("export")} result={exportResult} showError={false} />
<label> </div>
Import file
<input className="hidden" type="file" onChange={fileChanged} accept="application/json,text/plain" /> <div className="row">
</label> <button type="button" className="with-padding">
</button> <label>
</div> Import file
</form> <input
<form onSubmit={submitExport}> type="file"
<div className="row"> className="hidden"
<MutationButton name="export" label="Export" result={exportResult} showError={false} /> onChange={fileChanged}
<MutationButton name="export-file" label="Export file" result={exportResult} showError={false} /> accept="application/json,text/plain,text/csv"
/>
</label>
</button>
<div className="export-file">
<MutationButton label="Export to file" type="button" onClick={() => submitExport("export-file")} result={exportResult} showError={false} />
<span>
as
</span>
<Select <Select
field={form.exportType} field={form.exportType}
options={<> options={<>
<option value="plain">Text</option> <option value="plain">Text</option>
<option value="json">JSON</option> <option value="json">JSON</option>
<option value="csv">CSV</option>
</>} </>}
/> />
</div> </div>
</form> </div>
{parseResult.error && <Error error={parseResult.error} />} {parseResult.error && <Error error={parseResult.error} />}
{exportResult.error && <Error error={exportResult.error} />} {exportResult.error && <Error error={exportResult.error} />}
</div> </div>

View file

@ -21,6 +21,7 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const isValidDomain = require("is-valid-domain"); const isValidDomain = require("is-valid-domain");
const fileDownload = require("js-file-download"); const fileDownload = require("js-file-download");
const csv = require("papaparse");
const { const {
replaceCacheOnMutation, replaceCacheOnMutation,
@ -31,6 +32,23 @@ const {
function parseDomainList(list) { function parseDomainList(list) {
if (list[0] == "[") { if (list[0] == "[") {
return JSON.parse(list); return JSON.parse(list);
} else if (list.startsWith("#domain")) { // Mastodon CSV
const { data, errors } = csv.parse(list, {
header: true,
transformHeader: (header) => header.slice(1), // removes starting '#'
skipEmptyLines: true,
dynamicTyping: true
});
if (errors.length > 0) {
let error = "";
errors.forEach((err) => {
error += `${err.message} (line ${err.row})`;
});
throw error;
}
return data;
} else { } else {
return list.split("\n").map((line) => { return list.split("\n").map((line) => {
let domain = line.trim(); let domain = line.trim();
@ -109,6 +127,9 @@ module.exports = (build) => ({
}).then((exportList) => { }).then((exportList) => {
if (formData.exportType == "json") { if (formData.exportType == "json") {
return JSON.stringify(exportList); return JSON.stringify(exportList);
} else if (formData.exportType == "csv") {
let header = `#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate`;
} else { } else {
return exportList.join("\n"); return exportList.join("\n");
} }

View file

@ -712,6 +712,40 @@ button.with-padding {
} }
} }
.import-export {
.export-file {
display: flex;
gap: 0.7rem;
align-items: center;
}
}
.export-format-table {
background: $list-entry-alternate-bg;
border-collapse: collapse;
th, td {
border: 0.1rem solid $gray1;
padding: 0.3rem;
}
th {
background: $list-entry-bg;
}
td {
text-align: center;
.fa-check {
color: $green1;
}
.fa-times {
color: $error3;
}
}
}
.form-field.radio { .form-field.radio {
&, label { &, label {
display: flex; display: flex;

View file

@ -4117,6 +4117,11 @@ pako@~1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
papaparse@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467"
integrity sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"