mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 18:22:25 -06:00 
			
		
		
		
	CSV import/export, UI/UX improvements to import-export interface
This commit is contained in:
		
					parent
					
						
							
								3960327a43
							
						
					
				
			
			
				commit
				
					
						c80786014c
					
				
			
		
					 6 changed files with 165 additions and 23 deletions
				
			
		| 
						 | 
					@ -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",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										64
									
								
								web/source/settings/admin/federation/export-format-table.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/source/settings/admin/federation/export-format-table.jsx
									
										
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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");
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue