Skip to content

Commit

Permalink
comments: set maximum length for comments
Browse files Browse the repository at this point in the history
* UI: Add live character count validation to TimelineCommentEditor
* UI: Connect config value through Redux store to make it available as we make config value configurable, this allowe the max limit to change dynamically as well
* These changes depending on thins PR: inveniosoftware/react-invenio-forms#244
* Disable submit button when comment exceed char count
* Disable submit button when it's < 1 char
* Show understandable error msg when exceeding limit
  • Loading branch information
Samk13 committed Aug 13, 2024
1 parent 11ead41 commit f651cbc
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -20,7 +21,13 @@ import { Provider } from "react-redux";
export class InvenioRequestsApp extends Component {
constructor(props) {
super(props);
const { requestsApi, requestEventsApi, request, defaultQueryParams } = this.props;
const {
request,
requestsApi,
requestEventsApi,
defaultQueryParams,
commentContentMaxLength,
} = this.props;
const defaultRequestsApi = new InvenioRequestsAPI(
new RequestLinksExtractor(request)
);
Expand All @@ -32,8 +39,8 @@ export class InvenioRequestsApp extends Component {
requestEventsApi: requestEventsApi || defaultRequestEventsApi,
refreshIntervalMs: 5000,
defaultQueryParams,
commentContentMaxLength,
};

this.store = configureStore(appConfig);
}

Expand All @@ -52,12 +59,13 @@ export class InvenioRequestsApp extends Component {

InvenioRequestsApp.propTypes = {
requestsApi: PropTypes.object,
requestEventsApi: PropTypes.object,
overriddenCmps: PropTypes.object,
requestEventsApi: PropTypes.object,
request: PropTypes.object.isRequired,
userAvatar: PropTypes.string.isRequired,
defaultQueryParams: PropTypes.object,
userAvatar: PropTypes.string.isRequired,
permissions: PropTypes.object.isRequired,
commentContentMaxLength: PropTypes.number.isRequired,
};

InvenioRequestsApp.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ export class Request extends Component {
}

render() {
const { request, updateRequestAfterAction, userAvatar, permissions } = this.props;
const {
request,
userAvatar,
permissions,
updateRequestAfterAction,
} = this.props;

return (
<Overridable id="InvenioRequest.Request.layout">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 Northwestern University.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -55,7 +56,10 @@ const request = JSON.parse(requestDetailsDiv.dataset.record);
const defaultQueryParams = JSON.parse(requestDetailsDiv.dataset.defaultQueryConfig);
const userAvatar = JSON.parse(requestDetailsDiv.dataset.userAvatar);
const permissions = JSON.parse(requestDetailsDiv.dataset.permissions);

const commentContentMaxLength = parseInt(
requestDetailsDiv.dataset.commentContentMaxLength,
10
);
const defaultComponents = {
...defaultContribComponents,
"TimelineEvent.layout.unknown": TimelineUnknownEvent,
Expand Down Expand Up @@ -100,6 +104,7 @@ ReactDOM.render(
overriddenCmps={{ ...defaultComponents, ...overriddenComponents }}
userAvatar={userAvatar}
permissions={permissions}
commentContentMaxLength={commentContentMaxLength}
/>,
requestDetailsDiv
);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -16,12 +17,13 @@ const composeEnhancers = composeWithDevTools({

export function configureStore(config) {
const { size } = config.defaultQueryParams;
const { commentContentMaxLength } = config;

return createStore(
createReducers(),
// config object will be available in the actions,
{
timeline: { ...initialTimeLineState, size },
timeline: { ...initialTimeLineState, size, commentContentMaxLength },
},
composeEnhancers(applyMiddleware(thunk.withExtraArgument(config)))
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -19,7 +20,10 @@ const TimelineCommentEditor = ({
error,
submitComment,
userAvatar,
charCount,
commentContentMaxLength,
}) => {
const isSubmitDisabled = () => isLoading || (charCount === 0) || (charCount >= commentContentMaxLength)
return (
<div className="timeline-comment-editor-container">
{error && <Message negative>{error}</Message>}
Expand All @@ -43,6 +47,7 @@ const TimelineCommentEditor = ({
icon="send"
size="medium"
content={i18next.t("Comment")}
disabled={isSubmitDisabled()}
loading={isLoading}
onClick={() => submitComment(commentContent, "html")}
/>
Expand All @@ -58,13 +63,17 @@ TimelineCommentEditor.propTypes = {
error: PropTypes.string,
submitComment: PropTypes.func.isRequired,
userAvatar: PropTypes.string,
charCount: PropTypes.number,
commentContentMaxLength: PropTypes.number,
};

TimelineCommentEditor.defaultProps = {
commentContent: "",
isLoading: false,
error: "",
userAvatar: "",
charCount: 0,
commentContentMaxLength: 0,
};

export default TimelineCommentEditor;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -17,6 +18,8 @@ const mapStateToProps = (state) => ({
isLoading: state.timelineCommentEditor.isLoading,
error: state.timelineCommentEditor.error,
commentContent: state.timelineCommentEditor.commentContent,
charCount: state.timelineCommentEditor.charCount,
commentContentMaxLength: state.timeline.commentContentMaxLength,
});

export const TimelineCommentEditor = connect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -12,18 +13,33 @@ import {
SUCCESS as TIMELINE_SUCCESS,
} from "../../timeline/state/actions";
import _cloneDeep from "lodash/cloneDeep";
import { i18next } from "@translations/invenio_requests/i18next";

export const IS_LOADING = "eventEditor/IS_LOADING";
export const HAS_ERROR = "eventEditor/HAS_ERROR";
export const SUCCESS = "eventEditor/SUCCESS";
export const SETTING_CONTENT = "eventEditor/SETTING_CONTENT";
export const SET_CHAR_COUNT = "eventEditor/SET_CHAR_COUNT";

export const setEventContent = (content) => {
return async (dispatch, getState, config) => {
dispatch({
type: SETTING_CONTENT,
payload: content,
});

const commentContentMaxLength = config.commentContentMaxLength;
const charCount = content.length;
const errorMessage = i18next.t("Character count exceeds the maximum allowed limit.");

const validateContentLength = (charCount, maxLength, errorMessage) => {
return charCount >= maxLength ? errorMessage : null;
};

const error = validateContentLength(charCount, commentContentMaxLength, errorMessage);

dispatch({ type: SET_CHAR_COUNT, payload: charCount });
dispatch({ type: HAS_ERROR, payload: error });
};
};

Expand All @@ -44,7 +60,6 @@ export const submitComment = (content, format) => {
That includes the pagination logic e.g. changing pages if the current page size is exceeded by a new comment. */

const response = await config.requestsApi.submitComment(payload);

const currentPage = timelineState.page;
const currentSize = timelineState.size;
const currentCommentsLength = timelineState.data.hits.hits.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// This file is part of InvenioRequests
// Copyright (C) 2022 CERN.
// Copyright (C) 2024 KTH Royal Institute of Technology.
//
// Invenio RDM Records is free software; you can redistribute it and/or modify it
// under the terms of the MIT License; see LICENSE file for more details.

import { IS_LOADING, HAS_ERROR, SUCCESS, SETTING_CONTENT } from "./actions";
import { IS_LOADING, HAS_ERROR, SUCCESS, SETTING_CONTENT, SET_CHAR_COUNT } from "./actions";

const initial_state = {
error: null,
isLoading: false,
commentContent: "",
charCount: 0,
};

export const commentEditorReducer = (state = initial_state, action) => {
Expand All @@ -27,6 +29,8 @@ export const commentEditorReducer = (state = initial_state, action) => {
error: null,
commentContent: "",
};
case SET_CHAR_COUNT:
return { ...state, charCount: action.payload };
default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,47 @@ class TimelineCommentEvent extends Component {
const { event } = props;

this.state = {
commentContent: event?.payload?.content,
commentContent: event?.payload?.content || '',
inputError: null,
isDisabled: false,
commentContentMaxLength: 0,
charCount: event?.payload?.content?.length || 0,
};
}

componentDidMount() {
const requestDetailsDiv = document.getElementById("request-detail");
if (requestDetailsDiv) {
const commentContentMaxLength = parseInt(
requestDetailsDiv.dataset.commentContentMaxLength,
10
);
this.setState({ commentContentMaxLength });
}
}

validateContentLength = (charCount, maxLength) => {
const errorMessage = i18next.t("Character count exceeds the maximum allowed limit.");
return {
isDisabled: (charCount === 0) || (charCount >= maxLength),
inputError: charCount >= maxLength ? errorMessage : null,
};
};

handleEditorChange = (event, editor) => {
const content = editor.getContent();
const charCount = content.length;
const { commentContentMaxLength } = this.state;

const { isDisabled, inputError } = this.validateContentLength(charCount, commentContentMaxLength);

this.setState({
commentContent: content,
charCount,
isDisabled,
inputError,
});
};
eventToType = ({ type, payload }) => {
switch (type) {
case "L":
Expand All @@ -49,7 +86,7 @@ class TimelineCommentEvent extends Component {
deleteComment,
toggleEditMode,
} = this.props;
const { commentContent } = this.state;
const { commentContent, inputError } = this.state;

const commentHasBeenEdited = event?.revision_id > 1 && event?.payload;

Expand Down Expand Up @@ -109,15 +146,13 @@ class TimelineCommentEvent extends Component {
</Feed.Summary>

<Feed.Extra text={!isEditing}>
{error && <Error error={error} />}
{(error || inputError) && <Error error={error || inputError} />}

{isEditing ? (
<RichEditor
initialValue={event?.payload?.content}
inputValue={commentContent}
onEditorChange={(event, editor) => {
this.setState({ commentContent: editor.getContent() });
}}
onEditorChange={this.handleEditorChange}
minHeight={150}
/>
) : (
Expand All @@ -133,6 +168,7 @@ class TimelineCommentEvent extends Component {
<SaveButton
onClick={() => updateComment(commentContent, "html")}
loading={isLoading}
disabled={isLoading || this.state.isDisabled}
/>
</Container>
)}
Expand Down
4 changes: 4 additions & 0 deletions invenio_requests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# Copyright (C) 2021 CERN.
# Copyright (C) 2021 - 2022 TU Wien.
# Copyright (C) 2024 KTH Royal Institute of Technology.
#
# Invenio-Requests is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -117,3 +118,6 @@
"is_open": {"facet": facets.is_open, "ui": {"field": "is_open"}},
}
"""Available facets defined for this module."""

REQUESTS_COMMENT_CONTENT_MAX_LENGTH = 25000
"""Maximum length of a comment content."""
5 changes: 4 additions & 1 deletion invenio_requests/customizations/event_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022-2024 CERN.
# Copyright (C) 2024 KTH Royal Institute of Technology.
#
# Invenio-Requests is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -10,6 +11,7 @@
import inspect

import marshmallow as ma
from flask import current_app
from marshmallow import RAISE, fields, validate
from marshmallow_utils import fields as utils_fields

Expand Down Expand Up @@ -140,9 +142,10 @@ def payload_schema():
# we need to import here because of circular imports
from invenio_requests.records.api import RequestEventFormat

max = current_app.config.get("REQUESTS_COMMENT_CONTENT_MAX_LENGTH", 25000)
return dict(
content=utils_fields.SanitizedHTML(
required=True, validate=validate.Length(min=1)
required=True, validate=validate.Length(min=1, max=max)
),
format=fields.Str(
validate=validate.OneOf(choices=[e.value for e in RequestEventFormat]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This file is part of Invenio.
Copyright (C) 2016-2020 CERN.
Copyright (C) 2024 KTH Royal Institute of Technology.

Invenio is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -49,6 +50,7 @@ <h2 class="ui header">
data-default-query-config='{{ dict(size=config["REQUESTS_TIMELINE_PAGE_SIZE"]) | tojson }}'
data-user-avatar='{{ user_avatar | tojson }}'
data-permissions='{{ permissions | tojson }}'
data-comment-content-max-length='{{ config["REQUESTS_COMMENT_CONTENT_MAX_LENGTH"] }}'
>{# react app root #}
<div class="ui grid">
<div class="stretched row">
Expand Down
Loading

0 comments on commit f651cbc

Please sign in to comment.