From 20960d72387d35e5b43b15908ed8c412942fc0fa Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 29 Mar 2023 15:41:49 -0400 Subject: [PATCH 1/5] Improve UI of List component --- app/soapbox/components/list.tsx | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/soapbox/components/list.tsx b/app/soapbox/components/list.tsx index b48b24a87..ee04b2d92 100644 --- a/app/soapbox/components/list.tsx +++ b/app/soapbox/components/list.tsx @@ -4,8 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { SelectDropdown } from '../features/forms'; -import Icon from './icon'; -import { HStack, Select } from './ui'; +import { Icon, HStack, Select } from './ui'; interface IList { children: React.ReactNode @@ -58,13 +57,13 @@ const ListItem: React.FC = ({ label, hint, children, onClick, onSelec return (
- {label} + {label} {hint ? ( {hint} @@ -83,12 +82,26 @@ const ListItem: React.FC = ({ label, hint, children, onClick, onSelec
{children} - {isSelected ? ( +
- ) : null} +
) : null} From 85e57806450c6df10a094d0f2068d364c36244ad Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 29 Mar 2023 15:42:05 -0400 Subject: [PATCH 2/5] Deprecate old Icon component --- app/soapbox/components/icon.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/soapbox/components/icon.tsx b/app/soapbox/components/icon.tsx index 300265ea5..421d937dd 100644 --- a/app/soapbox/components/icon.tsx +++ b/app/soapbox/components/icon.tsx @@ -14,6 +14,9 @@ export interface IIcon extends React.HTMLAttributes { className?: string } +/** + * @deprecated Use the UI Icon component directly. + */ const Icon: React.FC = ({ src, alt, className, ...rest }) => { return (
Date: Wed, 29 Mar 2023 15:42:20 -0400 Subject: [PATCH 3/5] Add validation support to Group names --- .../manage-group-modal/manage-group-modal.tsx | 11 ++++- .../manage-group-modal/steps/details-step.tsx | 19 +++++--- .../manage-group-modal/steps/privacy-step.tsx | 4 +- .../hooks/api/groups/useGroupValidation.ts | 45 +++++++++++++++++++ app/soapbox/hooks/api/index.ts | 1 + 5 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 app/soapbox/hooks/api/groups/useGroupValidation.ts diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx index 2863060c5..fcc7c14da 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx @@ -3,7 +3,8 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { submitGroupEditor } from 'soapbox/actions/groups'; import { Modal, Stack } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useDebounce } from 'soapbox/hooks'; +import { useGroupValidation } from 'soapbox/hooks/api'; import ConfirmationStep from './steps/confirmation-step'; import DetailsStep from './steps/details-step'; @@ -34,6 +35,7 @@ interface IManageGroupModal { const ManageGroupModal: React.FC = ({ onClose }) => { const intl = useIntl(); + const debounce = useDebounce; const dispatch = useAppDispatch(); const id = useAppSelector((state) => state.group_editor.groupId); @@ -43,6 +45,11 @@ const ManageGroupModal: React.FC = ({ onClose }) => { const [currentStep, setCurrentStep] = useState(id ? Steps.TWO : Steps.ONE); + const name = useAppSelector((state) => state.group_editor.displayName); + const debouncedName = debounce(name, 300); + + const { data: { isValid } } = useGroupValidation(debouncedName); + const handleClose = () => { onClose('MANAGE_GROUP'); }; @@ -92,7 +99,7 @@ const ManageGroupModal: React.FC = ({ onClose }) => { : } confirmationAction={handleNextStep} confirmationText={confirmationText} - confirmationDisabled={isSubmitting} + confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)} confirmationFullWidth onClose={handleClose} > diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx index 2450dffea..56c755086 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx @@ -7,9 +7,9 @@ import { changeGroupEditorDescription, changeGroupEditorMedia, } from 'soapbox/actions/groups'; -import Icon from 'soapbox/components/icon'; -import { Avatar, Form, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; +import { Avatar, Form, FormGroup, HStack, Icon, Input, Text, Textarea } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector, useDebounce, useInstance } from 'soapbox/hooks'; +import { useGroupValidation } from 'soapbox/hooks/api'; import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts'; import resizeImage from 'soapbox/utils/resize-image'; @@ -30,7 +30,7 @@ const messages = defineMessages({ const HeaderPicker: React.FC = ({ src, onChange, accept, disabled }) => { return (
+ } + hintText={} + errors={isValid ? [] : [errorMessage as string]} > { maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))} /> + } > diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx index 22ad89305..2920de493 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx @@ -17,11 +17,11 @@ const PrivacyStep = () => { return ( <> - + - + diff --git a/app/soapbox/hooks/api/groups/useGroupValidation.ts b/app/soapbox/hooks/api/groups/useGroupValidation.ts new file mode 100644 index 000000000..e20499fd2 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupValidation.ts @@ -0,0 +1,45 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi'; + +type Validation = { + error: string + message: string +} + +const ValidationKeys = { + validation: (name: string) => ['group', 'validation', name] as const, +}; + +function useGroupValidation(name: string = '') { + const api = useApi(); + + const getValidation = async() => { + const { data } = await api.get('/api/v1/groups/validate', { + params: { name }, + }) + .catch((error) => { + if (error.response.status === 422) { + return { data: error.response.data }; + } + + throw error; + }); + + return data; + }; + + const queryInfo = useQuery(ValidationKeys.validation(name), getValidation, { + enabled: !!name, + }); + + return { + ...queryInfo, + data: { + ...queryInfo.data, + isValid: !queryInfo.data?.error, + }, + }; +} + +export { useGroupValidation }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index b70314633..936fda6ef 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -7,6 +7,7 @@ export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; export { useGroup, useGroups } from './groups/useGroups'; export { useGroupSearch } from './groups/useGroupSearch'; +export { useGroupValidation } from './groups/useGroupValidation'; export { useJoinGroup } from './groups/useJoinGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; From 455030ef5bc0eda510c4865d36ad3e3683ad3981 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 29 Mar 2023 15:44:00 -0400 Subject: [PATCH 4/5] Add validation support to features --- app/soapbox/hooks/api/groups/useGroupValidation.ts | 6 ++++-- app/soapbox/utils/features.ts | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/soapbox/hooks/api/groups/useGroupValidation.ts b/app/soapbox/hooks/api/groups/useGroupValidation.ts index e20499fd2..bfcd3bbb0 100644 --- a/app/soapbox/hooks/api/groups/useGroupValidation.ts +++ b/app/soapbox/hooks/api/groups/useGroupValidation.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks/useApi'; +import { useFeatures } from 'soapbox/hooks/useFeatures'; type Validation = { error: string @@ -13,6 +14,7 @@ const ValidationKeys = { function useGroupValidation(name: string = '') { const api = useApi(); + const features = useFeatures(); const getValidation = async() => { const { data } = await api.get('/api/v1/groups/validate', { @@ -30,14 +32,14 @@ function useGroupValidation(name: string = '') { }; const queryInfo = useQuery(ValidationKeys.validation(name), getValidation, { - enabled: !!name, + enabled: features.groupsValidation && !!name, }); return { ...queryInfo, data: { ...queryInfo.data, - isValid: !queryInfo.data?.error, + isValid: !queryInfo.data?.error ?? true, }, }; } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 5230546b5..8558e3b9d 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -554,6 +554,11 @@ const getInstanceFeatures = (instance: Instance) => { */ groupsSearch: v.software === TRUTHSOCIAL, + /** + * Can validate group names. + */ + groupsValidation: v.software === TRUTHSOCIAL, + /** * Can hide follows/followers lists and counts. * @see PATCH /api/v1/accounts/update_credentials From ff3c0c5cd7584abb7536ad8cd6b5bd4544f92181 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 30 Mar 2023 09:25:11 -0400 Subject: [PATCH 5/5] i18n --- app/soapbox/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index f8ac161d5..17d7268f2 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -942,6 +942,7 @@ "manage_group.edit_success": "The group was edited", "manage_group.fields.description_label": "Description", "manage_group.fields.description_placeholder": "Description", + "manage_group.fields.name_help": "This cannot be changed after the group is created.", "manage_group.fields.name_label": "Group name (required)", "manage_group.fields.name_placeholder": "Group Name", "manage_group.get_started": "Let’s get started!",