From 67187bb6a3ab2085f0f71ce5b272f4c0c58afa58 Mon Sep 17 00:00:00 2001 From: EstoesMoises Date: Sat, 26 Jul 2025 17:35:55 +0100 Subject: [PATCH 01/40] feat: added interface for EditorNotesPane --- .../src/components/Editor/EditorInterface.js | 89 +++++- .../Editor/EditorNotesPane/AddNoteForm.js | 162 +++++++++++ .../Editor/EditorNotesPane/EditorNotesPane.js | 164 ++++++++++++ .../Editor/EditorNotesPane/NoteItem.js | 253 ++++++++++++++++++ .../Editor/EditorNotesPane/NotesList.js | 50 ++++ packages/decap-cms-locales/src/en/index.js | 15 ++ 6 files changed, 730 insertions(+), 3 deletions(-) create mode 100644 packages/decap-cms-core/src/components/Editor/EditorNotesPane/AddNoteForm.js create mode 100644 packages/decap-cms-core/src/components/Editor/EditorNotesPane/EditorNotesPane.js create mode 100644 packages/decap-cms-core/src/components/Editor/EditorNotesPane/NoteItem.js create mode 100644 packages/decap-cms-core/src/components/Editor/EditorNotesPane/NotesList.js diff --git a/packages/decap-cms-core/src/components/Editor/EditorInterface.js b/packages/decap-cms-core/src/components/Editor/EditorInterface.js index cad473866aa2..02f5b2f961f2 100644 --- a/packages/decap-cms-core/src/components/Editor/EditorInterface.js +++ b/packages/decap-cms-core/src/components/Editor/EditorInterface.js @@ -16,12 +16,14 @@ import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync'; import EditorControlPane from './EditorControlPane/EditorControlPane'; import EditorPreviewPane from './EditorPreviewPane/EditorPreviewPane'; +import EditorNotesPane from './EditorNotesPane/EditorNotesPane'; import EditorToolbar from './EditorToolbar'; import { hasI18n, getI18nInfo, getPreviewEntry } from '../../lib/i18n'; import { FILES } from '../../constants/collectionTypes'; import { getFileFromSlug } from '../../reducers/collections'; const PREVIEW_VISIBLE = 'cms.preview-visible'; +const NOTES_VISIBLE = 'cms.notes-visible'; const SCROLL_SYNC_ENABLED = 'cms.scroll-sync-enabled'; const SPLIT_PANE_POSITION = 'cms.split-pane-position'; const I18N_VISIBLE = 'cms.i18n-visible'; @@ -122,6 +124,12 @@ const ControlPaneContainer = styled(PreviewPaneContainer)` overflow-x: hidden; `; +const NotesPaneContainer = styled.div` + height: 100%; + pointer-events: ${props => (props.blockEntry ? 'none' : 'auto')}; + overflow: hidden; +`; + const ViewControls = styled.div` position: absolute; top: 10px; @@ -132,12 +140,16 @@ const ViewControls = styled.div` function EditorContent({ i18nVisible, previewVisible, + notesVisible, editor, editorWithEditor, editorWithPreview, + editorWithNotes, }) { if (i18nVisible) { return editorWithEditor; + } else if (notesVisible) { + return editorWithNotes; } else if (previewVisible) { return editorWithPreview; } else { @@ -154,10 +166,20 @@ function isPreviewEnabled(collection, entry) { return collection.getIn(['editor', 'preview'], true); } +function isNotesEnabled(collection, entry) { + if (collection.get('type') === FILES) { + const file = getFileFromSlug(collection, entry.get('slug')); + const notesEnabled = file?.getIn(['editor', 'notes']); + if (notesEnabled != null) return notesEnabled; + } + return collection.getIn(['editor', 'notes'], true); +} + class EditorInterface extends Component { state = { showEventBlocker: false, previewVisible: localStorage.getItem(PREVIEW_VISIBLE) !== 'false', + notesVisible: localStorage.getItem(NOTES_VISIBLE) === 'false', scrollSyncEnabled: localStorage.getItem(SCROLL_SYNC_ENABLED) !== 'false', i18nVisible: localStorage.getItem(I18N_VISIBLE) !== 'false', }; @@ -190,8 +212,27 @@ class EditorInterface extends Component { handleTogglePreview = () => { const newPreviewVisible = !this.state.previewVisible; - this.setState({ previewVisible: newPreviewVisible }); + this.setState({ + previewVisible: newPreviewVisible, + notesVisible: false, // Hide notes when showing preview + }); localStorage.setItem(PREVIEW_VISIBLE, newPreviewVisible); + localStorage.setItem(NOTES_VISIBLE, 'false'); + }; + + handleToggleNotes = () => { + const newNotesVisible = !this.state.notesVisible; + this.setState({ + notesVisible: newNotesVisible, + previewVisible: false, // Hide preview when showing notes + }); + localStorage.setItem(NOTES_VISIBLE, newNotesVisible); + localStorage.setItem(PREVIEW_VISIBLE, 'false'); + }; + + handleNotesChange = (action, payload) => { + console.log('Notes change:', action, payload); + // TODO: Implement with Redux actions }; handleToggleScrollSync = () => { @@ -246,6 +287,7 @@ class EditorInterface extends Component { const { scrollSyncEnabled, showEventBlocker } = this.state; const previewEnabled = isPreviewEnabled(collection, entry); + const notesEnabled = isNotesEnabled(collection, entry); const { locales, defaultLocale } = getI18nInfo(this.props.collection); const collectionI18nEnabled = hasI18n(collection) && locales.length > 1; @@ -310,6 +352,33 @@ class EditorInterface extends Component { ); + const editorWithNotes = ( + +
+ + localStorage.setItem(SPLIT_PANE_POSITION, size)} + onDragStarted={this.handleSplitPaneDragStart} + onDragFinished={this.handleSplitPaneDragFinished} + > + {editor} + + + + +
+
+ ); + const editorWithEditor = (
@@ -329,7 +398,8 @@ class EditorInterface extends Component { const i18nVisible = collectionI18nEnabled && this.state.i18nVisible; const previewVisible = previewEnabled && this.state.previewVisible; - const scrollSyncVisible = i18nVisible || previewVisible; + const notesVisible = notesEnabled && this.state.notesVisible; + const scrollSyncVisible = i18nVisible || previewVisible || notesVisible; return ( @@ -386,6 +456,15 @@ class EditorInterface extends Component { title={t('editor.editorInterface.togglePreview')} /> )} + {notesEnabled && ( + + )} {scrollSyncVisible && !collection.getIn(['editor', 'visualEditing']) && ( @@ -439,6 +520,8 @@ EditorInterface.propTypes = { loadDeployPreview: PropTypes.func.isRequired, draftKey: PropTypes.string.isRequired, t: PropTypes.func.isRequired, + notes: ImmutablePropTypes.list, + onNotesChange: PropTypes.func, }; -export default EditorInterface; +export default EditorInterface; \ No newline at end of file diff --git a/packages/decap-cms-core/src/components/Editor/EditorNotesPane/AddNoteForm.js b/packages/decap-cms-core/src/components/Editor/EditorNotesPane/AddNoteForm.js new file mode 100644 index 000000000000..8e872f1e955e --- /dev/null +++ b/packages/decap-cms-core/src/components/Editor/EditorNotesPane/AddNoteForm.js @@ -0,0 +1,162 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styled from '@emotion/styled'; +import { colors, transitions } from 'decap-cms-ui-default'; + +const FormContainer = styled.div` + padding: 16px; + border-top: 1px solid ${colors.textFieldBorder}; + background-color: ${colors.inputBackground}; +`; + +const TextArea = styled.textarea` + width: 100%; + min-height: 80px; + padding: 12px; + border: 1px solid ${colors.textFieldBorder}; + border-radius: 4px; + font-size: 14px; + font-family: inherit; + line-height: 1.4; + resize: vertical; + outline: none; + transition: border-color ${transitions.main}; + + &:focus { + border-color: ${colors.active}; + box-shadow: 0 0 0 2px rgba(70, 151, 218, 0.1); + } + + &::placeholder { + color: ${colors.controlLabel}; + } +`; + +const FormActions = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; +`; + +const AddButton = styled.button` + background-color: ${colors.active}; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all ${transitions.main}; + + &:hover:not(:disabled) { + background-color: ${colors.activeAccent}; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +`; + +const CharCount = styled.span` + font-size: 12px; + color: ${props => props.warn ? colors.errorText : colors.controlLabel}; +`; + +const Hint = styled.p` + font-size: 12px; + color: ${colors.controlLabel}; + margin: 4px 0 0 0; + font-style: italic; +`; + +class AddNoteForm extends Component { + static propTypes = { + onAdd: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + }; + + state = { + content: '', + isFocused: false, + }; + + maxLength = 1000; + + handleContentChange = (e) => { + const content = e.target.value; + if (content.length <= this.maxLength) { + this.setState({ content }); + } + }; + + handleSubmit = (e) => { + e.preventDefault(); + const { content } = this.state; + const trimmedContent = content.trim(); + + if (trimmedContent) { + this.props.onAdd(trimmedContent); + this.setState({ content: '', isFocused: false }); + } + }; + + handleKeyDown = (e) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.handleSubmit(e); + } + }; + + handleFocus = () => { + this.setState({ isFocused: true }); + }; + + handleBlur = () => { + // Delay to allow button click to register + setTimeout(() => { + if (!this.state.content.trim()) { + this.setState({ isFocused: false }); + } + }, 150); + }; + + render() { + const { t } = this.props; + const { content } = this.state; + const charCount = content.length; + const canSubmit = content.trim().length > 0; + const isNearLimit = charCount > this.maxLength * 0.8; + + return ( + +
+