mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-22 10:12:11 +00:00
[Frontend] Settings for profile fields (#1885)
* get max emoji size from instance settings * expose (hardcoded) max amount of profile fields in instance api * basic profile field setting * fix profile field hook structure for updates * *twirls mustache* fix ze tests --------- Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
parent
4990099fde
commit
8fb5a7e7f8
|
@ -118,7 +118,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -221,7 +222,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -324,7 +326,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -478,7 +481,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -603,7 +607,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -743,7 +748,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
|
|
@ -54,6 +54,9 @@ type InstanceConfigurationAccounts struct {
|
|||
// The maximum number of featured tags allowed for each account.
|
||||
// Currently not implemented, so this is hardcoded to 10.
|
||||
MaxFeaturedTags int `json:"max_featured_tags"`
|
||||
// The maximum number of profile fields allowed for each account.
|
||||
// Currently not configurable, so this is hardcoded to 6. (https://github.com/superseriousbusiness/gotosocial/issues/1876)
|
||||
MaxProfileFields int `json:"max_profile_fields"`
|
||||
}
|
||||
|
||||
// InstanceConfigurationStatuses models instance status config parameters.
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
instancePollsMinExpiration = 300 // seconds
|
||||
instancePollsMaxExpiration = 2629746 // seconds
|
||||
instanceAccountsMaxFeaturedTags = 10
|
||||
instanceAccountsMaxProfileFields = 6 // FIXME: https://github.com/superseriousbusiness/gotosocial/issues/1876
|
||||
instanceSourceURL = "https://github.com/superseriousbusiness/gotosocial"
|
||||
)
|
||||
|
||||
|
@ -756,6 +757,7 @@ func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
|
|||
instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration
|
||||
instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
|
||||
instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
|
||||
instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
|
||||
instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize())
|
||||
|
||||
// URLs
|
||||
|
@ -882,6 +884,7 @@ func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
|
|||
instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration
|
||||
instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS()
|
||||
instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags
|
||||
instance.Configuration.Accounts.MaxProfileFields = instanceAccountsMaxProfileFields
|
||||
instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize())
|
||||
|
||||
// registrations
|
||||
|
|
|
@ -647,7 +647,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
|
@ -730,7 +731,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() {
|
|||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
"max_featured_tags": 10,
|
||||
"max_profile_fields": 6
|
||||
},
|
||||
"statuses": {
|
||||
"max_characters": 5000,
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"match-sorter": "^6.3.1",
|
||||
"modern-normalize": "^1.1.0",
|
||||
"nanoid": "^4.0.0",
|
||||
"object-to-formdata": "^4.4.2",
|
||||
"papaparse": "^5.3.2",
|
||||
"photoswipe": "^5.3.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
|
|
|
@ -42,9 +42,14 @@ const MutationButton = require("../../../components/form/mutation-button");
|
|||
module.exports = function NewEmojiForm() {
|
||||
const shortcode = useShortcode();
|
||||
|
||||
const { data: instance } = query.useInstanceQuery();
|
||||
const emojiMaxSize = React.useMemo(() => {
|
||||
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
|
||||
}, [instance]);
|
||||
|
||||
const image = useFileInput("image", {
|
||||
withPreview: true,
|
||||
maxSize: 50 * 1024 // TODO: get from instance api?
|
||||
maxSize: emojiMaxSize
|
||||
});
|
||||
|
||||
const category = useComboBoxInput("category");
|
||||
|
|
33
web/source/settings/lib/form/context.jsx
Normal file
33
web/source/settings/lib/form/context.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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 FormContext = React.createContext({});
|
||||
|
||||
module.exports = {
|
||||
FormContext,
|
||||
useWithFormContext(index, form) {
|
||||
const formContainer = React.useContext(FormContext);
|
||||
formContainer[index] = form;
|
||||
return form;
|
||||
}
|
||||
};
|
65
web/source/settings/lib/form/field-array.jsx
Normal file
65
web/source/settings/lib/form/field-array.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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 getFormMutations = require("./get-form-mutations");
|
||||
|
||||
function parseFields(entries, length) {
|
||||
const fields = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (entries[i] != undefined) {
|
||||
fields[i] = Object.assign({}, entries[i]);
|
||||
} else {
|
||||
fields[i] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
module.exports = function useArrayInput({ name, _Name }, { initialValue, length = 0 }) {
|
||||
const fields = React.useRef({});
|
||||
|
||||
const value = React.useMemo(() => parseFields(initialValue, length), [initialValue, length]);
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
ctx: fields.current,
|
||||
maxLength: length,
|
||||
selectedValues() {
|
||||
// if any form field changed, we need to re-send everything
|
||||
const hasUpdate = Object.values(fields.current).some((fieldSet) => {
|
||||
const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
|
||||
return updatedFields.length > 0;
|
||||
});
|
||||
if (hasUpdate) {
|
||||
return Object.values(fields.current).map((fieldSet) => {
|
||||
return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
47
web/source/settings/lib/form/get-form-mutations.js
Normal file
47
web/source/settings/lib/form/get-form-mutations.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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 syncpipe = require("syncpipe");
|
||||
|
||||
module.exports = function getFormMutations(form, { changedOnly }) {
|
||||
let updatedFields = [];
|
||||
return {
|
||||
updatedFields,
|
||||
mutationData: syncpipe(form, [
|
||||
(_) => Object.values(_),
|
||||
(_) => _.map((field) => {
|
||||
if (field.selectedValues != undefined) {
|
||||
let selected = field.selectedValues();
|
||||
if (!changedOnly || selected.length > 0) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, selected];
|
||||
}
|
||||
} else if (!changedOnly || field.hasChanged()) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, field.value];
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
(_) => _.filter((value) => value != null),
|
||||
(_) => Object.fromEntries(_)
|
||||
])
|
||||
};
|
||||
};
|
|
@ -74,6 +74,7 @@ module.exports = {
|
|||
useRadioInput: makeHook(require("./radio")),
|
||||
useComboBoxInput: makeHook(require("./combo-box")),
|
||||
useCheckListInput: makeHook(require("./check-list")),
|
||||
useFieldArrayInput: makeHook(require("./field-array")),
|
||||
useValue: function (name, value) {
|
||||
return {
|
||||
name,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const syncpipe = require("syncpipe");
|
||||
const getFormMutations = require("./get-form-mutations");
|
||||
|
||||
module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true, onFinish } = {}) {
|
||||
if (!Array.isArray(mutationQuery)) {
|
||||
|
@ -44,25 +44,12 @@ module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = tru
|
|||
}
|
||||
usedAction.current = action;
|
||||
// transform the field definitions into an object with just their values
|
||||
let updatedFields = [];
|
||||
const mutationData = syncpipe(form, [
|
||||
(_) => Object.values(_),
|
||||
(_) => _.map((field) => {
|
||||
if (field.selectedValues != undefined) {
|
||||
let selected = field.selectedValues();
|
||||
if (!changedOnly || selected.length > 0) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, selected];
|
||||
}
|
||||
} else if (!changedOnly || field.hasChanged()) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, field.value];
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
(_) => _.filter((value) => value != null),
|
||||
(_) => Object.fromEntries(_)
|
||||
]);
|
||||
|
||||
const { mutationData, updatedFields } = getFormMutations(form, { changedOnly });
|
||||
|
||||
if (updatedFields.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutationData.action = action;
|
||||
|
||||
|
|
|
@ -20,23 +20,7 @@
|
|||
"use strict";
|
||||
|
||||
const { createApi, fetchBaseQuery } = require("@reduxjs/toolkit/query/react");
|
||||
const { isPlainObject } = require("is-plain-object");
|
||||
|
||||
function convertToForm(obj) {
|
||||
const formData = new FormData();
|
||||
Object.entries(obj).forEach(([key, val]) => {
|
||||
if (isPlainObject(val)) {
|
||||
Object.entries(val).forEach(([key2, val2]) => {
|
||||
if (val2 != undefined) {
|
||||
formData.set(`${key}[${key2}]`, val2);
|
||||
}
|
||||
});
|
||||
} else if (val != undefined) {
|
||||
formData.set(key, val);
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
const { serialize: serializeForm } = require("object-to-formdata");
|
||||
|
||||
function instanceBasedQuery(args, api, extraOptions) {
|
||||
const state = api.getState();
|
||||
|
@ -55,7 +39,9 @@ function instanceBasedQuery(args, api, extraOptions) {
|
|||
|
||||
if (args.asForm) {
|
||||
delete args.asForm;
|
||||
args.body = convertToForm(args.body);
|
||||
args.body = serializeForm(args.body, {
|
||||
indices: true, // Array indices, for profile fields
|
||||
});
|
||||
}
|
||||
|
||||
return fetchBaseQuery({
|
||||
|
|
|
@ -439,6 +439,17 @@ section.with-sidebar > div, section.with-sidebar > form {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
|
@ -26,10 +26,12 @@ const query = require("../lib/query");
|
|||
const {
|
||||
useTextInput,
|
||||
useFileInput,
|
||||
useBoolInput
|
||||
useBoolInput,
|
||||
useFieldArrayInput
|
||||
} = require("../lib/form");
|
||||
|
||||
const useFormSubmit = require("../lib/form/submit");
|
||||
const { useWithFormContext, FormContext } = require("../lib/form/context");
|
||||
|
||||
const {
|
||||
TextInput,
|
||||
|
@ -65,8 +67,11 @@ function UserProfileForm({ data: profile }) {
|
|||
*/
|
||||
|
||||
const { data: instance } = query.useInstanceQuery();
|
||||
const allowCustomCSS = React.useMemo(() => {
|
||||
return instance?.configuration?.accounts?.allow_custom_css === true;
|
||||
const instanceConfig = React.useMemo(() => {
|
||||
return {
|
||||
allowCustomCSS: instance?.configuration?.accounts?.allow_custom_css === true,
|
||||
maxPinnedFields: instance?.configuration?.accounts?.max_profile_fields ?? 6
|
||||
};
|
||||
}, [instance]);
|
||||
|
||||
const form = {
|
||||
|
@ -78,9 +83,18 @@ function UserProfileForm({ data: profile }) {
|
|||
bot: useBoolInput("bot", { source: profile }),
|
||||
locked: useBoolInput("locked", { source: profile }),
|
||||
enableRSS: useBoolInput("enable_rss", { source: profile }),
|
||||
fields: useFieldArrayInput("fields_attributes", {
|
||||
defaultValue: profile?.source?.fields,
|
||||
length: instanceConfig.maxPinnedFields
|
||||
}),
|
||||
};
|
||||
|
||||
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
||||
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation(), {
|
||||
onFinish: () => {
|
||||
form.avatar.reset();
|
||||
form.header.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<form className="user-profile" onSubmit={submitForm}>
|
||||
|
@ -129,7 +143,11 @@ function UserProfileForm({ data: profile }) {
|
|||
field={form.enableRSS}
|
||||
label="Enable RSS feed of Public posts"
|
||||
/>
|
||||
{!allowCustomCSS ? null :
|
||||
<b>Profile fields</b>
|
||||
<ProfileFields
|
||||
field={form.fields}
|
||||
/>
|
||||
{!instanceConfig.allowCustomCSS ? null :
|
||||
<TextArea
|
||||
field={form.customCSS}
|
||||
label="Custom CSS"
|
||||
|
@ -142,4 +160,40 @@ function UserProfileForm({ data: profile }) {
|
|||
<MutationButton label="Save profile info" result={result} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileFields({ field: formField }) {
|
||||
return (
|
||||
<div className="fields">
|
||||
<FormContext.Provider value={formField.ctx}>
|
||||
{formField.value.map((data, i) => (
|
||||
<Field
|
||||
key={i}
|
||||
index={i}
|
||||
data={data}
|
||||
/>
|
||||
))}
|
||||
</FormContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ index, data }) {
|
||||
const form = useWithFormContext(index, {
|
||||
name: useTextInput("name", { defaultValue: data.name }),
|
||||
value: useTextInput("value", { defaultValue: data.value })
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="entry">
|
||||
<TextInput
|
||||
field={form.name}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<TextInput
|
||||
field={form.value}
|
||||
placeholder="Value"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4137,6 +4137,11 @@ object-keys@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object-to-formdata@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.4.2.tgz#f89013f90493c58cb5f6ab9f50b7aeec30745ea6"
|
||||
integrity sha512-fu6UDjsqIfFUu/B3GXJ2IFnNAL/YbsC1PPzqDIFXcfkhdYjTD3K4zqhyD/lZ6+KdP9O/64YIPckIOiS5ouXwLA==
|
||||
|
||||
object.assign@^4.1.3, object.assign@^4.1.4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
|
||||
|
|
Loading…
Reference in a new issue