0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added error handling pattern for social accounts setting

refs https://github.com/TryGhost/Team/issues/3318

Adds error handling pattern for facebook and twitter account settings same as old admin, showing error message when incorrect value is added and also on blur updates the value to include the facebook/twitter URL
This commit is contained in:
Rishabh 2023-06-01 22:05:52 +05:30
parent 1cce47482f
commit cbf486d811
4 changed files with 151 additions and 51 deletions

View file

@ -44,7 +44,8 @@
"@ebay/nice-modal-react": "^1.2.10",
"@tryghost/timezone-data": "0.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"validator": "7.2.0"
},
"devDependencies": {
"@storybook/addon-essentials": "7.0.18",
@ -59,6 +60,7 @@
"@tailwindcss/line-clamp": "0.4.4",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/validator": "^13.7.17",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"@vitejs/plugin-react": "4.0.0",

View file

@ -15,20 +15,22 @@ interface TextFieldProps {
hint?: React.ReactNode;
clearBg?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
maxLength?: number;
}
const TextField: React.FC<TextFieldProps> = ({
type = 'text',
inputRef,
title,
value,
error,
placeholder,
hint,
clearBg = false,
onChange,
type = 'text',
inputRef,
title,
value,
error,
placeholder,
hint,
clearBg = false,
onChange,
onBlur,
className = '',
maxLength,
...props
@ -37,12 +39,13 @@ const TextField: React.FC<TextFieldProps> = ({
<div className='flex flex-col'>
{title && <Heading useLabelTag={true}>{title}</Heading>}
<input
ref={inputRef}
className={`border-b ${!clearBg && 'bg-grey-100 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-300 hover:border-grey-400 focus:border-black`} ${(title && !clearBg) && `mt-2`} ${className}`}
defaultValue={value}
maxLength={maxLength}
ref={inputRef}
className={`border-b ${!clearBg && 'bg-grey-100 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-300 hover:border-grey-400 focus:border-black`} ${(title && !clearBg) && `mt-2`} ${className}`}
defaultValue={value}
maxLength={maxLength}
placeholder={placeholder}
type={type}
onBlur={onBlur}
onChange={onChange}
{...props} />
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}

View file

@ -1,28 +1,83 @@
import React, {useContext, useState} from 'react';
import React, {useRef, useState} from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../admin-x-ds/global/TextField';
import {SettingsContext} from '../../providers/SettingsProvider';
import {TSettingGroupStates} from '../../../admin-x-ds/settings/SettingGroup';
import {getSettingValue} from '../../../utils/helpers';
import useSettingGroup from '../../../hooks/useSettingGroup';
import validator from 'validator';
function validateFacebookUrl(newUrl: string) {
const errMessage = 'The URL must be in a format like https://www.facebook.com/yourPage';
if (!newUrl) {
return '';
}
// strip any facebook URLs out
newUrl = newUrl.replace(/(https?:\/\/)?(www\.)?facebook\.com/i, '');
// don't allow any non-facebook urls
if (newUrl.match(/^(http|\/\/)/i)) {
throw new Error(errMessage);
}
// strip leading / if we have one then concat to full facebook URL
newUrl = newUrl.replace(/^\//, '');
newUrl = `https://www.facebook.com/${newUrl}`;
// don't allow URL if it's not valid
if (!validator.isURL(newUrl)) {
throw new Error(errMessage);
}
return newUrl;
}
function validateTwitterUrl(newUrl: string) {
if (!newUrl) {
return '';
}
if (newUrl.match(/(?:twitter\.com\/)(\S+)/) || newUrl.match(/([a-z\d.]+)/i)) {
let username = [];
if (newUrl.match(/(?:twitter\.com\/)(\S+)/)) {
[, username] = newUrl.match(/(?:twitter\.com\/)(\S+)/);
} else {
[username] = newUrl.match(/([^/]+)\/?$/mi);
}
// check if username starts with http or www and show error if so
if (username.match(/^(http|www)|(\/)/) || !username.match(/^[a-z\d._]{1,15}$/mi)) {
const message = !username.match(/^[a-z\d._]{1,15}$/mi)
? 'Your Username is not a valid Twitter Username'
: 'The URL must be in a format like https://twitter.com/yourUsername';
throw new Error(message);
}
return `https://twitter.com/${username}`;
} else {
const message = 'The URL must be in a format like https://twitter.com/yourUsername';
throw new Error(message);
}
}
const SocialAccounts: React.FC = () => {
const [currentState, setCurrentState] = useState<TSettingGroupStates>('view');
const {
currentState,
saveState,
handleSave,
handleCancel,
updateSetting,
focusRef,
getSettingValues,
handleStateChange
} = useSettingGroup();
const handleStateChange = (newState: TSettingGroupStates) => {
setCurrentState(newState);
};
const [errors, setErrors] = useState<{
facebook?: string;
twitter?: string;
}>({});
const {settings, saveSettings} = useContext(SettingsContext) || {};
const savedFacebookUser = (getSettingValue(settings, 'facebook') || '') as string;
const savedTwitterUser = (getSettingValue(settings, 'twitter') || '') as string;
let [, fbUser] = savedFacebookUser.match(/(\S+)/) || [];
let [, twitterUser] = savedTwitterUser.match(/@?([^/]*)/) || [];
const twitterInputRef = useRef<HTMLInputElement>(null);
const savedFacebookUrl = `https://www.facebook.com/${fbUser}`;
const savedTwitterUrl = `https://twitter.com/${twitterUser}`;
const [facebookUrl, setFacebookUrl] = useState(savedFacebookUrl);
const [twitterUrl, setTwitterUrl] = useState(savedTwitterUrl);
const [facebookUrl, twitterUrl] = getSettingValues(['facebook', 'twitter']) as string[];
const values = (
<SettingGroupContent
@ -42,28 +97,56 @@ const SocialAccounts: React.FC = () => {
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, type:'facebook' | 'twitter') => {
setCurrentState('unsaved');
handleStateChange('unsaved');
if (type === 'facebook') {
setFacebookUrl(e.target.value);
updateSetting('facebook', e.target.value);
} else {
setTwitterUrl(e.target.value);
updateSetting('twitter', e.target.value);
}
};
const inputs = (
<SettingGroupContent>
<TextField
error={!!errors.facebook}
hint={errors.facebook}
inputRef={focusRef}
placeholder="https://www.facebook.com/ghost"
title={`URL of your publication's Facebook Page`}
value={facebookUrl}
onBlur={(e) => {
try {
const newUrl = validateFacebookUrl(e.target.value);
updateSetting('facebook', newUrl);
if (focusRef.current) {
focusRef.current.value = newUrl;
}
} catch (err: any) {
// ignore error
}
}}
onChange={(e) => {
handleChange(e, 'facebook');
}}
/>
<TextField
error={!!errors.twitter}
hint={errors.twitter}
inputRef={twitterInputRef}
placeholder="https://twitter.com/ghost"
title="URL of your Twitter profile"
value={twitterUrl}
onBlur={(e) => {
try {
const newUrl = validateTwitterUrl(e.target.value);
updateSetting('twitter', newUrl);
if (twitterInputRef.current) {
twitterInputRef.current.value = newUrl;
}
} catch (err: any) {
// ignore error
}
}}
onChange={(e) => {
handleChange(e, 'twitter');
}}
@ -71,29 +154,36 @@ const SocialAccounts: React.FC = () => {
</SettingGroupContent>
);
const handleSave = () => {
let [, facebookUser] = facebookUrl.match(/(?:https:\/\/)(?:www\.)(?:facebook\.com)\/(?:#!\/)?(\w+\/?\S+)/mi) || [];
let [, twUser] = twitterUrl.match(/(?:https:\/\/)(?:twitter\.com)\/(?:#!\/)?@?([^/]*)/) || [];
saveSettings?.([
{
key: 'facebook',
value: facebookUser
},
{
key: 'twitter',
value: `@${twUser}`
}
]);
setCurrentState('view');
};
return (
<SettingGroup
description='Link your social accounts for full structured data and rich card support'
navid='social-accounts'
saveState={saveState}
state={currentState}
title='Social accounts'
onSave={handleSave}
onCancel={handleCancel}
onSave={() => {
const formErrors: {
facebook?: string;
twitter?: string;
} = {};
try {
validateFacebookUrl(facebookUrl);
} catch (e: any) {
formErrors.facebook = e?.message;
}
try {
validateTwitterUrl(twitterUrl);
} catch (e: any) {
formErrors.twitter = e?.message;
}
setErrors(formErrors);
if (Object.keys(formErrors).length === 0) {
handleSave();
}
}}
onStateChange={handleStateChange}
>
{currentState === 'view' ? values : inputs}

View file

@ -7783,6 +7783,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@types/validator@^13.7.17":
version "13.7.17"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.17.tgz#0a6d1510395065171e3378a4afc587a3aefa7cc1"
integrity sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==
"@types/ws@^8.5.1":
version "8.5.4"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"