diff options
| author | 2023-08-19 14:33:15 +0200 | |
|---|---|---|
| committer | 2023-08-19 14:33:15 +0200 | |
| commit | 92de8fb396265d057f18aab4de0bc8aff4b90188 (patch) | |
| tree | 46438b9ff550261f56aa58d038cdf2f1e15493e3 /web/source | |
| parent | [bugfix] fix double firing bun.DB query hooks (#2124) (diff) | |
| download | gotosocial-92de8fb396265d057f18aab4de0bc8aff4b90188.tar.xz | |
[feature] Instance rules (#2125)
* init instance rules database model, admin api
* expose instance rules in public instance api
* public /api/v1/instance/rules route
* GET ruleById
* createRule route
* createRule auth check
* updateRule
* deleteRule
* list rules on about page
* ruleGet auth
* add about page ids for anchors
* process and store adding violated rules to reports
* admin api models for instance rules
* instance rule edit frontend
* change rule inputs to textareas
* database fixes after rebase (#2124)
* remove unused imports
* fix db migration column name
* fix tests
* fix more tests
* fix postgres error with wrongly used Ident
* add some tests, fiddle with rule model a bit, fix postgres migration
* swagger docs
---------
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
Diffstat (limited to 'web/source')
| -rw-r--r-- | web/source/css/base.css | 51 | ||||
| -rw-r--r-- | web/source/settings/admin/federation/detail.js | 35 | ||||
| -rw-r--r-- | web/source/settings/admin/settings/index.jsx (renamed from web/source/settings/admin/settings.js) | 12 | ||||
| -rw-r--r-- | web/source/settings/admin/settings/rules.jsx | 169 | ||||
| -rw-r--r-- | web/source/settings/index.js | 5 | ||||
| -rw-r--r-- | web/source/settings/lib/query/admin/index.js | 48 | ||||
| -rw-r--r-- | web/source/settings/lib/query/base.js | 2 | ||||
| -rw-r--r-- | web/source/settings/lib/query/lib.js | 7 | 
8 files changed, 306 insertions, 23 deletions
| diff --git a/web/source/css/base.css b/web/source/css/base.css index 87d2fcca7..5cd2cd047 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -542,6 +542,57 @@ label {  	}  } +.instance-rules { +	list-style-position: inside; +	margin: 0; +	padding: 0; + +	a.rule { +		display: grid; +		grid-template-columns: 1fr auto; +		align-items: center; +		color: $fg; +		text-decoration: none; +		background: $toot-bg; +		padding: 1rem; +		margin: 0.5rem 0; +		border-radius: $br; +		line-height: 2rem; +		position: relative; + +		&:hover { +			color: $fg-accent; + +			.edit-icon { +				display: inline; +			} +		} + +		.edit-icon { +			display: none; +			font-size: 1rem; +			line-height: 1.5rem; +		} + +		li { +			font-size: 1.75rem; +			padding: 0; +			margin: 0; + +			h2 { +				margin: 0; +				margin-top: 0 !important; +				display: inline-block; +				font-size: 1.5rem; +			} +		} + +		span { +			color: $fg-reduced; +		} +	} +} +  @media screen and (max-width: 30rem) {  	.domain-blocklist .entry {  		grid-template-columns: 1fr; diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js index 344b9f9b6..a3bbfcac1 100644 --- a/web/source/settings/admin/federation/detail.js +++ b/web/source/settings/admin/federation/detail.js @@ -141,22 +141,29 @@ function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {  				{...disabledForm}  			/> -			<MutationButton -				label="Suspend" -				result={addResult} -				{...disabledForm} -			/> - -			{ -				isExistingBlock && +			<div className="action-buttons row">  				<MutationButton -					type="button" -					onClick={() => removeBlock(block.id)} -					label="Remove" -					result={removeResult} -					className="button danger" +					label="Suspend" +					result={addResult} +					showError={false} +					{...disabledForm}  				/> -			} + +				{ +					isExistingBlock && +					<MutationButton +						type="button" +						onClick={() => removeBlock(block.id)} +						label="Remove" +						result={removeResult} +						className="button danger" +						showError={false} +					/> +				} +			</div> + +			{addResult.error && <Error error={addResult.error} />} +			{removeResult.error && <Error error={removeResult.error} />}  		</form>  	); diff --git a/web/source/settings/admin/settings.js b/web/source/settings/admin/settings/index.jsx index ec986a6c4..dab476433 100644 --- a/web/source/settings/admin/settings.js +++ b/web/source/settings/admin/settings/index.jsx @@ -21,23 +21,23 @@  const React = require("react"); -const query = require("../lib/query"); +const query = require("../../lib/query");  const {  	useTextInput,  	useFileInput -} = require("../lib/form"); +} = require("../../lib/form"); -const useFormSubmit = require("../lib/form/submit"); +const useFormSubmit = require("../../lib/form/submit");  const {  	TextInput,  	TextArea,  	FileInput -} = require("../components/form/inputs"); +} = require("../../components/form/inputs"); -const FormWithData = require("../lib/form/form-with-data"); -const MutationButton = require("../components/form/mutation-button"); +const FormWithData = require("../../lib/form/form-with-data"); +const MutationButton = require("../../components/form/mutation-button");  module.exports = function AdminSettings() {  	return ( diff --git a/web/source/settings/admin/settings/rules.jsx b/web/source/settings/admin/settings/rules.jsx new file mode 100644 index 000000000..330bc07fd --- /dev/null +++ b/web/source/settings/admin/settings/rules.jsx @@ -0,0 +1,169 @@ +/* +	GoToSocial +	Copyright (C) GoToSocial Authors admin@gotosocial.org +	SPDX-License-Identifier: AGPL-3.0-or-later + +	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"); +const { Switch, Route, Link, Redirect, useRoute } = require("wouter"); + +const query = require("../../lib/query"); +const FormWithData = require("../../lib/form/form-with-data"); +const { useBaseUrl } = require("../../lib/navigation/util"); + +const { useValue, useTextInput } = require("../../lib/form"); +const useFormSubmit = require("../../lib/form/submit"); + +const { TextArea } = require("../../components/form/inputs"); +const MutationButton = require("../../components/form/mutation-button"); + +module.exports = function InstanceRulesData({ baseUrl }) { +	return ( +		<FormWithData +			dataQuery={query.useInstanceRulesQuery} +			DataForm={InstanceRules} +			baseUrl={baseUrl} +		/> +	); +}; + +function InstanceRules({ baseUrl, data: rules }) { +	return ( +		<Switch> +			<Route path={`${baseUrl}/:ruleId`}> +				<InstanceRuleDetail rules={rules} /> +			</Route> +			<Route> +				<div> +					<h1>Instance Rules</h1> +					<div> +						<p> +							The rules for your instance are listed on the about page, and can be selected when submitting reports. +						</p> +					</div> +					<InstanceRuleList rules={rules} /> +				</div> +			</Route> +		</Switch> +	); +} + +function InstanceRuleList({ rules }) { +	const newRule = useTextInput("text", {}); + +	const [submitForm, result] = useFormSubmit({ newRule }, query.useAddInstanceRuleMutation(), { +		onFinish: () => newRule.reset() +	}); + +	return ( +		<> +			<form onSubmit={submitForm} className="new-rule"> +				<ol className="instance-rules"> +					{Object.values(rules).map((rule) => ( +						<InstanceRule key={rule.id} rule={rule} /> +					))} +				</ol> +				<TextArea +					field={newRule} +					label="New instance rule" +				/> +				<MutationButton label="Add rule" result={result} /> +			</form> +		</> +	); +} + +function InstanceRule({ rule }) { +	const baseUrl = useBaseUrl(); + +	return ( +		<Link to={`${baseUrl}/${rule.id}`}> +			<a className="rule"> +				<li> +					<h2>{rule.text} <i className="fa fa-pencil edit-icon" /></h2> +				</li> +				<span>{new Date(rule.created_at).toLocaleString()}</span> +			</a> +		</Link> +	); +} + +function InstanceRuleDetail({ rules }) { +	const baseUrl = useBaseUrl(); +	let [_match, params] = useRoute(`${baseUrl}/:ruleId`); + +	if (params?.ruleId == undefined || rules[params.ruleId] == undefined) { +		return <Redirect to={baseUrl} />; +	} else { +		return ( +			<> +				<Link to={baseUrl}><a>< go back</a></Link> +				<InstanceRuleForm rule={rules[params.ruleId]} /> +			</> +		); +	} +} + +function InstanceRuleForm({ rule }) { +	const baseUrl = useBaseUrl(); +	const form = { +		id: useValue("id", rule.id), +		rule: useTextInput("text", { defaultValue: rule.text }) +	}; + +	const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceRuleMutation()); + +	const [deleteRule, deleteResult] = query.useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id }); + +	if (result.isSuccess || deleteResult.isSuccess) { +		return ( +			<Redirect to={baseUrl} /> +		); +	} + +	return ( +		<div className="rule-detail"> +			<form onSubmit={submitForm}> +				<TextArea +					field={form.rule} +				/> + +				<div className="action-buttons row"> +					<MutationButton +						label="Save" +						showError={false} +						result={result} +						disabled={!form.rule.hasChanged()} +					/> + +					<MutationButton +						type="button" +						onClick={() => deleteRule(rule.id)} +						label="Delete" +						className="button danger" +						showError={false} +						result={deleteResult} +					/> +				</div> + +				{result.error && <Error error={result.error} />} +				{deleteResult.error && <Error error={deleteResult.error} />} +			</form> +		</div> +	); +}
\ No newline at end of file diff --git a/web/source/settings/index.js b/web/source/settings/index.js index 8eb11e0aa..398bca0f6 100644 --- a/web/source/settings/index.js +++ b/web/source/settings/index.js @@ -60,7 +60,10 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [  			Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),  			Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))  		]), -		Item("Settings", { icon: "fa-sliders" }, require("./admin/settings")) +		Menu("Settings", { icon: "fa-sliders" }, [ +			Item("Settings", { icon: "fa-sliders", url: "" }, require("./admin/settings")), +			Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules")) +		])  	])  ]); diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js index dd4a61b51..515d8edcf 100644 --- a/web/source/settings/lib/query/admin/index.js +++ b/web/source/settings/lib/query/admin/index.js @@ -22,7 +22,8 @@  const {  	replaceCacheOnMutation,  	removeFromCacheOnMutation, -	domainListToObject +	domainListToObject, +	idListToObject  } = require("../lib");  const base = require("../base"); @@ -104,6 +105,51 @@ const endpoints = (build) => ({  			return res.accounts ?? [];  		}  	}), +	instanceRules: build.query({ +		query: () => ({ +			url: `/api/v1/admin/instance/rules` +		}), +		transformResponse: idListToObject +	}), +	addInstanceRule: build.mutation({ +		query: (formData) => ({ +			method: "POST", +			url: `/api/v1/admin/instance/rules`, +			asForm: true, +			body: formData, +			discardEmpty: true +		}), +		transformResponse: (data) => { +			return { +				[data.id]: data +			}; +		}, +		...replaceCacheOnMutation("instanceRules") +	}), +	updateInstanceRule: build.mutation({ +		query: ({ id, ...edit }) => ({ +			method: "PATCH", +			url: `/api/v1/admin/instance/rules/${id}`, +			asForm: true, +			body: edit, +			discardEmpty: true +		}), +		transformResponse: (data) => { +			return { +				[data.id]: data +			}; +		}, +		...replaceCacheOnMutation("instanceRules") +	}), +	deleteInstanceRule: build.mutation({ +		query: (id) => ({ +			method: "DELETE", +			url: `/api/v1/admin/instance/rules/${id}` +		}), +		...removeFromCacheOnMutation("instanceRules", { +			findKey: (_draft, rule) => rule.id +		}) +	}),  	...require("./import-export")(build),  	...require("./custom-emoji")(build),  	...require("./reports")(build) diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js index 653fc449b..ba02d4e07 100644 --- a/web/source/settings/lib/query/base.js +++ b/web/source/settings/lib/query/base.js @@ -59,7 +59,7 @@ function instanceBasedQuery(args, api, extraOptions) {  module.exports = createApi({  	reducerPath: "api",  	baseQuery: instanceBasedQuery, -	tagTypes: ["Auth", "Emoji", "Reports", "Account"], +	tagTypes: ["Auth", "Emoji", "Reports", "Account", "InstanceRules"],  	endpoints: (build) => ({  		instance: build.query({  			query: () => ({ diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js index 78a9291b7..56ce05478 100644 --- a/web/source/settings/lib/query/lib.js +++ b/web/source/settings/lib/query/lib.js @@ -37,6 +37,13 @@ module.exports = {  			(_) => Object.fromEntries(_)  		]);  	}, +	idListToObject: (data) => { +		// Turn flat Array into Object keyed by entry id field +		return syncpipe(data, [ +			(_) => _.map((entry) => [entry.id, entry]), +			(_) => Object.fromEntries(_) +		]); +	},  	replaceCacheOnMutation: makeCacheMutation((draft, newData) => {  		Object.assign(draft, newData);  	}), | 
