mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-13 22:41:32 -05:00
Added Enter as shortcut to go next, when adding a recommendation (#18381)
closes https://github.com/TryGhost/Product/issues/3953
This commit is contained in:
parent
488ef87a7d
commit
3d91c37d5b
4 changed files with 66 additions and 40 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, {useId} from 'react';
|
||||
import React, {useEffect, useId} from 'react';
|
||||
|
||||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
|
@ -22,6 +22,7 @@ interface TextAreaProps {
|
|||
fontStyle?: FontStyles;
|
||||
className?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const TextArea: React.FC<TextAreaProps> = ({
|
||||
|
@ -51,6 +52,14 @@ const TextArea: React.FC<TextAreaProps> = ({
|
|||
setFocusState(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.autoFocus && inputRef && inputRef.current) {
|
||||
const textarea = inputRef.current;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||
}
|
||||
}, [props.autoFocus, inputRef]);
|
||||
|
||||
let styles = clsx(
|
||||
'peer order-2 rounded-sm border px-3 py-2 dark:text-white',
|
||||
clearBg ? 'bg-transparent' : 'bg-grey-75 dark:bg-grey-950',
|
||||
|
|
|
@ -2,8 +2,8 @@ import AddRecommendationModalConfirm from './AddRecommendationModalConfirm';
|
|||
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import URLTextField from '../../../../admin-x-ds/global/form/URLTextField';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {AlreadyExistsError} from '../../../../utils/errors';
|
||||
|
@ -20,13 +20,14 @@ interface AddRecommendationModalProps {
|
|||
}
|
||||
|
||||
const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModalProps> = ({recommendation, animate}) => {
|
||||
const [enterPressed, setEnterPressed] = useState(false);
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const {query: queryOembed} = useGetOembed();
|
||||
const {query: queryExternalGhostSite} = useExternalGhostSite();
|
||||
const {query: getRecommendationByUrl} = useGetRecommendationByUrl();
|
||||
|
||||
const {formState, updateForm, handleSave, errors, validate, saveState, clearError} = useForm({
|
||||
const {formState, updateForm, handleSave, errors, saveState, clearError} = useForm({
|
||||
initialState: recommendation ?? {
|
||||
title: '',
|
||||
url: '',
|
||||
|
@ -111,12 +112,38 @@ const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModa
|
|||
}
|
||||
});
|
||||
|
||||
let okLabel = 'Next';
|
||||
let loadingState = false;
|
||||
const saveForm = async () => {
|
||||
if (saveState === 'saving') {
|
||||
// Already saving
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveState === 'saving') {
|
||||
loadingState = true;
|
||||
}
|
||||
dismissAllToasts();
|
||||
try {
|
||||
await handleSave({force: true});
|
||||
} catch (e) {
|
||||
const message = e instanceof AlreadyExistsError ? e.message : 'Something went wrong while checking this URL, please try again.';
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (enterPressed) {
|
||||
saveForm();
|
||||
setEnterPressed(false); // Reset for future use
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formState]);
|
||||
|
||||
const formatUrl = (url: string) => {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
return <Modal
|
||||
afterClose={() => {
|
||||
|
@ -126,49 +153,37 @@ const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModa
|
|||
animate={animate ?? true}
|
||||
backDropClick={false}
|
||||
okColor='black'
|
||||
okLabel={okLabel}
|
||||
okLoading={loadingState}
|
||||
okLabel={'Next'}
|
||||
okLoading={saveState === 'saving'}
|
||||
size='sm'
|
||||
testId='add-recommendation-modal'
|
||||
title='Add recommendation'
|
||||
onOk={async () => {
|
||||
if (saveState === 'saving') {
|
||||
// Already saving
|
||||
return;
|
||||
}
|
||||
|
||||
dismissAllToasts();
|
||||
try {
|
||||
await handleSave({force: true});
|
||||
} catch (e) {
|
||||
const message = e instanceof AlreadyExistsError ? e.message : 'Something went wrong while checking this URL, please try again.';
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message
|
||||
});
|
||||
}
|
||||
}}
|
||||
onOk={saveForm}
|
||||
>
|
||||
<p className="mt-4">You can recommend any site your audience will find valuable, not just those published on Ghost.</p>
|
||||
<Form
|
||||
marginBottom={false}
|
||||
marginTop
|
||||
>
|
||||
<URLTextField
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
error={Boolean(errors.url)}
|
||||
hint={errors.url || <>Need inspiration? <a className='text-green' href="https://www.ghost.org/explore" rel="noopener noreferrer" target='_blank'>Explore thousands of sites</a> to recommend</>}
|
||||
placeholder='https://www.example.com'
|
||||
title='URL'
|
||||
value={formState.url}
|
||||
onBlur={validate}
|
||||
onChange={u => updateForm((state) => {
|
||||
return {
|
||||
...state,
|
||||
url: u
|
||||
};
|
||||
})}
|
||||
onKeyDown={() => clearError?.('url')}
|
||||
onBlur={() => updateForm(state => ({...state, url: formatUrl(formState.url)}))}
|
||||
onChange={(e) => {
|
||||
clearError?.('url');
|
||||
updateForm(state => ({...state, url: e.target.value}));
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
updateForm(state => ({...state, url: formatUrl(formState.url)}));
|
||||
setEnterPressed(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import Hint from '../../../../admin-x-ds/global/Hint';
|
||||
import React from 'react';
|
||||
import React, {useRef} from 'react';
|
||||
import RecommendationIcon from './RecommendationIcon';
|
||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
|
@ -20,7 +20,7 @@ interface Props<T extends EditOrAddRecommendation> {
|
|||
const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recommendation>> = ({showURL, formState, updateForm, errors, clearError}) => {
|
||||
const [reasonLength, setReasonLength] = React.useState(formState?.reason?.length || 0);
|
||||
const reasonLengthColor = reasonLength > 200 ? 'text-red' : 'text-green';
|
||||
|
||||
const focusRef = useRef<HTMLTextAreaElement>(null);
|
||||
return <Form
|
||||
marginBottom={false}
|
||||
marginTop
|
||||
|
@ -63,9 +63,11 @@ const RecommendationReasonForm: React.FC<Props<EditOrAddRecommendation | Recomme
|
|||
}}
|
||||
/>
|
||||
<TextArea
|
||||
autoFocus={true}
|
||||
clearBg={true}
|
||||
error={Boolean(errors.reason)}
|
||||
hint={errors.reason || <>Max. <strong>200</strong> characters. You've used <strong className={reasonLengthColor}>{reasonLength}</strong></>}
|
||||
inputRef={focusRef}
|
||||
rows={3}
|
||||
title="Short description"
|
||||
value={formState.reason ?? ''}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 4d3319d05ce92e7b0244e5608d3fc6cc9c86e735
|
||||
Subproject commit 276e2c9d0140c902e1c8d3760bc194790722fa71
|
Loading…
Add table
Reference in a new issue