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:
parent
1cce47482f
commit
cbf486d811
4 changed files with 151 additions and 51 deletions
|
@ -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",
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue