diff --git a/studio-ui/ui/app/src/components/FormsEngine/FormsEngine.tsx b/studio-ui/ui/app/src/components/FormsEngine/FormsEngine.tsx index cd52de559..ebe8f2c48 100644 --- a/studio-ui/ui/app/src/components/FormsEngine/FormsEngine.tsx +++ b/studio-ui/ui/app/src/components/FormsEngine/FormsEngine.tsx @@ -96,6 +96,7 @@ import { displayFormBeingSavedSnack, fetchUpdateRequirements, generateDefaultChangesComment, + generateDefaultCreationComment, getAdditionalFieldsIdsFromDescriptor, resolveControlDescriptors, getCurrentChildFormStateSummary, @@ -104,6 +105,7 @@ import { internalLockContentService, internalUnlockContentService, prepareEmbeddedItemForm, + produceCreationMessage, setFieldAtoms, useUnlockOnClose, useValidateFormProps @@ -286,6 +288,7 @@ function FormBootstrap(props: FormsEngineProps) { const triggerReload = useCallback(() => setReloadNonce((nonce) => nonce + 1), []); const effectiveUpdatePath = renamedPath ?? update?.path; const customControls = useSelection((state) => state.uiConfig.controls); + const { formatMessage } = useIntl(); const contextApi = useMemo(() => { const getInitialValues = () => stableFormContextRef.current.originalValues; @@ -491,7 +494,9 @@ function FormBootstrap(props: FormsEngineProps) { lockResult: lockResultAtom, readonly: atom(false), expandedStateBySectionId: buildSectionExpandedStateAtoms(contentType.sections), - fileName: atom('') + fileName: atom(''), + // Default version comment for new content. + versionComment: atom(produceCreationMessage('', formatMessage)) }); const contentObject = createObjectWithSystemProps(contentType); const values = createParsedValuesObject( @@ -718,8 +723,12 @@ function FormOrchestrator(props: FormsEngineProps) { const effectRefs = useUpdateRefs({ fieldsToRender, versionCommentAtom: stableFormContext.atoms.versionComment, + fileNameAtom: stableFormContext.atoms.fileName, lockStatus }); + // Holds the create mode comment generated last, so that a comment written by the user isn't overwritten. Starts off + // with the default comment the version comment atom was created with. + const lastCreationCommentRef = useRef(produceCreationMessage('', formatMessage)); const [collapseHeader, setCollapseHeader] = useState(false); const [saveAsDraftAction, setSaveAsDraftAction] = useState(false); const [invalidForm, setInvalidForm] = useState(false); @@ -752,9 +761,22 @@ function FormOrchestrator(props: FormsEngineProps) { // String-type fields have auto-rollback detection; the fieldUpdates$ will emit anyway. Checking if the fieldId // emitted is in changedFieldIds should tell if the field was rolled back. setHasPendingChanges(changedFieldIds.size > 0); - // No comment generation for content creation. - if (isCreateMode) return; const versionCommentAtom = effectRefs.current.versionCommentAtom; + // Create mode bases the comment off of the page URL (file-name) instead of the fields changed. Note that any + // field update re-runs this since every field's validation depends on the file name. + if (isCreateMode) { + const newMessage = generateDefaultCreationComment( + store.get(effectRefs.current.fileNameAtom), + store.get(versionCommentAtom).trim(), + lastCreationCommentRef.current, + formatMessage + ); + if (newMessage) { + lastCreationCommentRef.current = newMessage; + store.set(versionCommentAtom, newMessage); + } + return; + } const newMessage = generateDefaultChangesComment( contentType.fields, effectRefs.current.fieldsToRender, @@ -766,7 +788,16 @@ function FormOrchestrator(props: FormsEngineProps) { return () => { sub.unsubscribe(); }; - }, [changedFieldIds, contentType.fields, effectRefs, setHasPendingChanges, fieldUpdates$, store, isCreateMode]); + }, [ + changedFieldIds, + contentType.fields, + effectRefs, + setHasPendingChanges, + fieldUpdates$, + store, + isCreateMode, + formatMessage + ]); const sourceMapPaths = useMemo(() => Object.values(sourceMap ?? []).sort(), [sourceMap]); useFetchContentItems(sourceMapPaths); diff --git a/studio-ui/ui/app/src/components/FormsEngine/lib/formUtils.tsx b/studio-ui/ui/app/src/components/FormsEngine/lib/formUtils.tsx index 82f3dec7e..265e91498 100644 --- a/studio-ui/ui/app/src/components/FormsEngine/lib/formUtils.tsx +++ b/studio-ui/ui/app/src/components/FormsEngine/lib/formUtils.tsx @@ -796,6 +796,48 @@ export function generateDefaultChangesComment( return newMessage; } +/** + * Produces the "save comment" for content being created at the supplied page URL (file name). An empty page URL + * produces the generic comment new content forms start off with. + **/ +export function produceCreationMessage(pageUrl: string, formatMessage: IntlShape['formatMessage']): string { + return pageUrl + ? formatMessage({ defaultMessage: 'Created {pageUrl}' }, { pageUrl }) + : formatMessage({ defaultMessage: 'Created content' }); +} + +/** + * Generates the default "save comment" for content being created, based on the page URL (file name) it will be + * created at. + * @param pageUrl The current value of the file name field. + * @param currentMessage The version comment currently held by the form. + * @param lastGeneratedMessage The comment this function generated last, used to detect user input on the comment. + * @returns The new comment, or undefined when the comment should be left untouched. + **/ +export function generateDefaultCreationComment( + pageUrl: string, + currentMessage: string, + lastGeneratedMessage: string, + formatMessage: IntlShape['formatMessage'] +): string | undefined { + const newMessage = pageUrl + ? formatMessage({ defaultMessage: 'Created {pageUrl}' }, { pageUrl }) + : formatMessage({ defaultMessage: 'Created content' }); + if ( + // Nothing to change + currentMessage === newMessage || + // If message is blank, no point in checking if the user has altered the message. + (currentMessage !== '' && + // The version comment has been manually altered by the user (i.e. if the current message isn't the last + // message generated here, we can assume the message has been altered by user input) + currentMessage !== lastGeneratedMessage) + ) { + // Do not set a new message + return; + } + return newMessage; +} + /** * Creates a summary of the state the current stacked form being rendered (last one on the stack) **/