mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +00:00 
			
		
		
		
	enhancement: cmd+enter and ctrl+enter to save entry (#22311)
* enhancement: keyboard shortcuts to save entry * chore: add e2e test * chore: restore validate fn * fix: don't register cmd+s for arc browser * fix: remove cmd+s and ctrl+s
This commit is contained in:
		
							parent
							
								
									d6bba97c7e
								
							
						
					
					
						commit
						03640aa70e
					
				@ -768,6 +768,139 @@ const UpdateAction: DocumentActionComponent = ({
 | 
			
		||||
  const setErrors = useForm('UpdateAction', (state) => state.setErrors);
 | 
			
		||||
  const resetForm = useForm('PublishAction', ({ resetForm }) => resetForm);
 | 
			
		||||
 | 
			
		||||
  const handleUpdate = React.useCallback(async () => {
 | 
			
		||||
    setSubmitting(true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (!modified) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { errors } = await validate(true, {
 | 
			
		||||
        status: 'draft',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (errors) {
 | 
			
		||||
        toggleNotification({
 | 
			
		||||
          type: 'danger',
 | 
			
		||||
          message: formatMessage({
 | 
			
		||||
            id: 'content-manager.validation.error',
 | 
			
		||||
            defaultMessage:
 | 
			
		||||
              'There are validation errors in your document. Please fix them before saving.',
 | 
			
		||||
          }),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isCloning) {
 | 
			
		||||
        const res = await clone(
 | 
			
		||||
          {
 | 
			
		||||
            model,
 | 
			
		||||
            documentId: cloneMatch.params.origin!,
 | 
			
		||||
            params,
 | 
			
		||||
          },
 | 
			
		||||
          transformData(document)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if ('data' in res) {
 | 
			
		||||
          navigate(
 | 
			
		||||
            {
 | 
			
		||||
              pathname: `../${res.data.documentId}`,
 | 
			
		||||
              search: rawQuery,
 | 
			
		||||
            },
 | 
			
		||||
            { relative: 'path' }
 | 
			
		||||
          );
 | 
			
		||||
        } else if (
 | 
			
		||||
          'error' in res &&
 | 
			
		||||
          isBaseQueryError(res.error) &&
 | 
			
		||||
          res.error.name === 'ValidationError'
 | 
			
		||||
        ) {
 | 
			
		||||
          setErrors(formatValidationErrors(res.error));
 | 
			
		||||
        }
 | 
			
		||||
      } else if (documentId || collectionType === SINGLE_TYPES) {
 | 
			
		||||
        const res = await update(
 | 
			
		||||
          {
 | 
			
		||||
            collectionType,
 | 
			
		||||
            model,
 | 
			
		||||
            documentId,
 | 
			
		||||
            params,
 | 
			
		||||
          },
 | 
			
		||||
          transformData(document)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if ('error' in res && isBaseQueryError(res.error) && res.error.name === 'ValidationError') {
 | 
			
		||||
          setErrors(formatValidationErrors(res.error));
 | 
			
		||||
        } else {
 | 
			
		||||
          resetForm();
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const res = await create(
 | 
			
		||||
          {
 | 
			
		||||
            model,
 | 
			
		||||
            params,
 | 
			
		||||
          },
 | 
			
		||||
          transformData(document)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if ('data' in res && collectionType !== SINGLE_TYPES) {
 | 
			
		||||
          navigate(
 | 
			
		||||
            {
 | 
			
		||||
              pathname: `../${res.data.documentId}`,
 | 
			
		||||
              search: rawQuery,
 | 
			
		||||
            },
 | 
			
		||||
            { replace: true, relative: 'path' }
 | 
			
		||||
          );
 | 
			
		||||
        } else if (
 | 
			
		||||
          'error' in res &&
 | 
			
		||||
          isBaseQueryError(res.error) &&
 | 
			
		||||
          res.error.name === 'ValidationError'
 | 
			
		||||
        ) {
 | 
			
		||||
          setErrors(formatValidationErrors(res.error));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      setSubmitting(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    clone,
 | 
			
		||||
    cloneMatch?.params.origin,
 | 
			
		||||
    collectionType,
 | 
			
		||||
    create,
 | 
			
		||||
    document,
 | 
			
		||||
    documentId,
 | 
			
		||||
    formatMessage,
 | 
			
		||||
    formatValidationErrors,
 | 
			
		||||
    isCloning,
 | 
			
		||||
    model,
 | 
			
		||||
    modified,
 | 
			
		||||
    navigate,
 | 
			
		||||
    params,
 | 
			
		||||
    rawQuery,
 | 
			
		||||
    resetForm,
 | 
			
		||||
    setErrors,
 | 
			
		||||
    setSubmitting,
 | 
			
		||||
    toggleNotification,
 | 
			
		||||
    update,
 | 
			
		||||
    validate,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  // Auto-save on CMD+S or CMD+Enter on macOS, and CTRL+S or CTRL+Enter on Windows/Linux
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const handleKeyDown = (e: KeyboardEvent) => {
 | 
			
		||||
      if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        handleUpdate();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.addEventListener('keydown', handleKeyDown);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('keydown', handleKeyDown);
 | 
			
		||||
    };
 | 
			
		||||
  }, [handleUpdate]);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    /**
 | 
			
		||||
     * Disabled when:
 | 
			
		||||
@ -780,101 +913,7 @@ const UpdateAction: DocumentActionComponent = ({
 | 
			
		||||
      id: 'global.save',
 | 
			
		||||
      defaultMessage: 'Save',
 | 
			
		||||
    }),
 | 
			
		||||
    onClick: async () => {
 | 
			
		||||
      setSubmitting(true);
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const { errors } = await validate(true, {
 | 
			
		||||
          status: 'draft',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (errors) {
 | 
			
		||||
          toggleNotification({
 | 
			
		||||
            type: 'danger',
 | 
			
		||||
            message: formatMessage({
 | 
			
		||||
              id: 'content-manager.validation.error',
 | 
			
		||||
              defaultMessage:
 | 
			
		||||
                'There are validation errors in your document. Please fix them before saving.',
 | 
			
		||||
            }),
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isCloning) {
 | 
			
		||||
          const res = await clone(
 | 
			
		||||
            {
 | 
			
		||||
              model,
 | 
			
		||||
              documentId: cloneMatch.params.origin!,
 | 
			
		||||
              params,
 | 
			
		||||
            },
 | 
			
		||||
            transformData(document)
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if ('data' in res) {
 | 
			
		||||
            navigate(
 | 
			
		||||
              {
 | 
			
		||||
                pathname: `../${res.data.documentId}`,
 | 
			
		||||
                search: rawQuery,
 | 
			
		||||
              },
 | 
			
		||||
              { relative: 'path' }
 | 
			
		||||
            );
 | 
			
		||||
          } else if (
 | 
			
		||||
            'error' in res &&
 | 
			
		||||
            isBaseQueryError(res.error) &&
 | 
			
		||||
            res.error.name === 'ValidationError'
 | 
			
		||||
          ) {
 | 
			
		||||
            setErrors(formatValidationErrors(res.error));
 | 
			
		||||
          }
 | 
			
		||||
        } else if (documentId || collectionType === SINGLE_TYPES) {
 | 
			
		||||
          const res = await update(
 | 
			
		||||
            {
 | 
			
		||||
              collectionType,
 | 
			
		||||
              model,
 | 
			
		||||
              documentId,
 | 
			
		||||
              params,
 | 
			
		||||
            },
 | 
			
		||||
            transformData(document)
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (
 | 
			
		||||
            'error' in res &&
 | 
			
		||||
            isBaseQueryError(res.error) &&
 | 
			
		||||
            res.error.name === 'ValidationError'
 | 
			
		||||
          ) {
 | 
			
		||||
            setErrors(formatValidationErrors(res.error));
 | 
			
		||||
          } else {
 | 
			
		||||
            resetForm();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          const res = await create(
 | 
			
		||||
            {
 | 
			
		||||
              model,
 | 
			
		||||
              params,
 | 
			
		||||
            },
 | 
			
		||||
            transformData(document)
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if ('data' in res && collectionType !== SINGLE_TYPES) {
 | 
			
		||||
            navigate(
 | 
			
		||||
              {
 | 
			
		||||
                pathname: `../${res.data.documentId}`,
 | 
			
		||||
                search: rawQuery,
 | 
			
		||||
              },
 | 
			
		||||
              { replace: true, relative: 'path' }
 | 
			
		||||
            );
 | 
			
		||||
          } else if (
 | 
			
		||||
            'error' in res &&
 | 
			
		||||
            isBaseQueryError(res.error) &&
 | 
			
		||||
            res.error.name === 'ValidationError'
 | 
			
		||||
          ) {
 | 
			
		||||
            setErrors(formatValidationErrors(res.error));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } finally {
 | 
			
		||||
        setSubmitting(false);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    onClick: handleUpdate,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -250,6 +250,11 @@ test.describe('Edit View', () => {
 | 
			
		||||
      await page.getByRole('button', { name: 'Save' }).click();
 | 
			
		||||
      await findAndClose(page, 'Saved Document');
 | 
			
		||||
 | 
			
		||||
      // Check that we can save with keyboard shortcuts
 | 
			
		||||
      await page.getByRole('textbox', { name: 'title' }).fill('Being an American...');
 | 
			
		||||
      await page.keyboard.press('Control+Enter');
 | 
			
		||||
      await findAndClose(page, 'Saved Document');
 | 
			
		||||
 | 
			
		||||
      await expect(page.getByRole('tab', { name: 'Draft' })).toHaveAttribute(
 | 
			
		||||
        'aria-selected',
 | 
			
		||||
        'true'
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user