Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notes for Case items #737

Merged
merged 61 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
7593979
Add note to case + corresponding embeds
drio18 Jul 27, 2023
7e2d1f9
Merge remote-tracking branch 'origin/master' into cfm-dash_comments
crfmc Jul 31, 2023
b2a05e8
- feat: component for displaying note property
crfmc Aug 1, 2023
013dc6c
style: saved and unsaved identifiers for note icon
crfmc Aug 1, 2023
c051c5b
- feat: pass note object down
crfmc Aug 1, 2023
aa73401
style: format the note text preview in new module
crfmc Aug 1, 2023
c1b79a8
- fix: fill the notes icon
crfmc Aug 1, 2023
07ce2b7
Merging in babel changes
crfmc Aug 3, 2023
7b89740
Merge remote-tracking branch 'origin' into cfm-dash_comments
crfmc Aug 4, 2023
0dab451
style: data-status colors for indicators
crfmc Aug 4, 2023
5a4a747
- style: adjust position for unsaved note dot
crfmc Aug 4, 2023
e6288d0
feat: Case Notes Column component button and open
crfmc Aug 4, 2023
11177da
- style: add focus outline
crfmc Aug 10, 2023
25345d8
- fix: update the title of the column
crfmc Aug 10, 2023
00c7807
Merge remote-tracking branch 'origin' into cfm-dash_comments
crfmc Aug 14, 2023
12613c3
style: adjust dimensions of column/button/popover
crfmc Aug 14, 2023
e49ce2d
feat: passing down props, adding state
crfmc Aug 14, 2023
39e646b
style: adjusting text size and padding
crfmc Aug 14, 2023
3fc4659
- style: increase padding above popover body
crfmc Sep 5, 2023
72ea879
Merge remote-tracking branch 'origin' into cfm-dash_comments
crfmc Sep 5, 2023
e90168d
Merge remote-tracking branch 'origin' into cfm-dash_comments
crfmc Sep 6, 2023
ffea7e9
- feat: show "save note" when there is no note
crfmc Sep 14, 2023
30bda3e
- feat: delete note item when empty string
crfmc Sep 15, 2023
d47df44
- chore: delete unused ParentProp
crfmc Sep 20, 2023
d81e471
Add note to case + corresponding embeds
drio18 Jul 27, 2023
69dd590
- feat: component for displaying note property
crfmc Aug 1, 2023
2c895cd
style: saved and unsaved identifiers for note icon
crfmc Aug 1, 2023
c02fcb0
- feat: pass note object down
crfmc Aug 1, 2023
d82eb0b
style: format the note text preview in new module
crfmc Aug 1, 2023
0750084
- fix: fill the notes icon
crfmc Aug 1, 2023
a85d0de
style: data-status colors for indicators
crfmc Aug 4, 2023
9311cd6
- style: adjust position for unsaved note dot
crfmc Aug 4, 2023
43489c2
feat: Case Notes Column component button and open
crfmc Aug 4, 2023
73d2829
- style: add focus outline
crfmc Aug 10, 2023
07fd081
- fix: update the title of the column
crfmc Aug 10, 2023
ef6fb66
style: adjust dimensions of column/button/popover
crfmc Aug 14, 2023
b56eb58
feat: passing down props, adding state
crfmc Aug 14, 2023
dffc876
style: adjusting text size and padding
crfmc Aug 14, 2023
db2c88b
- style: increase padding above popover body
crfmc Sep 5, 2023
661bc98
- feat: show "save note" when there is no note
crfmc Sep 14, 2023
11febf9
- feat: delete note item when empty string
crfmc Sep 15, 2023
fc40bfc
- chore: delete unused ParentProp
crfmc Sep 20, 2023
217a136
Merge branch 'cfm-dash_comments' of https://github.com/dbmi-bgm/cgap-…
crfmc Sep 20, 2023
2b0e673
- feat: change note icon outline when unsaved
crfmc Sep 20, 2023
32e8c43
- style: add attribute for showing red icon
crfmc Sep 21, 2023
2527cc5
chore: bump changelog and project version
crfmc Sep 25, 2023
610694e
- chore: adding comments
crfmc Sep 25, 2023
9feebbc
chore: additional clarifying comment
crfmc Sep 25, 2023
60b38d8
- style: move styles into search module
crfmc Sep 29, 2023
6c1bd1e
- feat: update conditional for note indicator rendering
crfmc Oct 2, 2023
1440669
fix: remove loading icon
crfmc Oct 2, 2023
45668f8
fix: add momentary loading state to button
crfmc Oct 3, 2023
7729a53
- chore: remove unused code
crfmc Oct 3, 2023
ab74a3c
fix: version number and changelog
crfmc Oct 3, 2023
ce0c692
- chore: remove unnecessary comments
crfmc Oct 3, 2023
8bb1f22
fix: prevent page overflow by popover
crfmc Oct 11, 2023
4f1b8db
- fix: change syntax for popover flip field
crfmc Oct 11, 2023
130befb
Merge remote-tracking branch 'origin/master' into cfm-dash_comments
crfmc Oct 11, 2023
535519e
Merge remote-tracking branch 'origin/master' into cfm-dash_comments
crfmc Oct 13, 2023
4aa2aad
chore: bump version number
crfmc Oct 13, 2023
65ff450
fix: remove point about scss module in changelog
crfmc Oct 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ cgap-portal
Change Log
----------

14.3.2
======
`PR 737: Add notes for Case items <https://github.com/dbmi-bgm/cgap-portal/pull/737>`_

* Adds functionality for POSTing and PATCHing Note Items linked to Case Items
* Reveals new "Notes" column in columnExtensionMap
* New "notes available" indicator in search headers column
* New textarea popup component (CaseNotesColumn.js) for text input
* New SCSS module (_case-notes.scss) for styling the above


14.3.1
======
`PR 753: Auth0 Symlink Bugfix <https://github.com/dbmi-bgm/cgap-portal/pull/753>`_
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
# Note: Various modules refer to this system as "encoded", not "cgap-portal".
name = "encoded"
version = "14.3.1"
version = "14.3.2"
crfmc marked this conversation as resolved.
Show resolved Hide resolved
description = "Computational Genome Analysis Platform"
authors = ["4DN-DCIC Team <[email protected]>"]
license = "MIT"
Expand Down
19 changes: 19 additions & 0 deletions src/encoded/schemas/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@
"exclude_from": [
"FFedit-create"
]
},
"note": {
"title": "Note",
"description": "Notes for this case",
"type": "string",
"linkTo": "NoteStandard"
}
},
"facets": {
Expand Down Expand Up @@ -329,6 +335,19 @@
"title": "Report Last Modified Date"
}
]
},
"notes": {
"title": "Case Notes",
"sort_fields": [
{
"field": "note.last_text_edited.date_text_edited",
"title": "Last Edit Date"
},
{
"field": "note.last_text_edited.text_edited_by.display_title",
"title": "Last Edit User"
}
]
crfmc marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
323 changes: 323 additions & 0 deletions src/encoded/static/components/browse/CaseNotesColumn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import React, { useState, useRef, useEffect, forwardRef } from "react";
import { Popover, OverlayTrigger } from "react-bootstrap";
import { LocalizedTime } from "@hms-dbmi-bgm/shared-portal-components/es/components/ui/LocalizedTime";
import { ajax } from "@hms-dbmi-bgm/shared-portal-components/es/components/util";


/**
* React-Boostrap (v1.6.7, Bootstrap 4.6 syntax) Popover component containing
* the main user actions, such as the "save" button and the textarea element
* where users add note text.
*/
const CaseNotesPopover = forwardRef(({
note,
lastSavedText,
handleNoteSave,
currentText,
setCurrentText,
buttonRef,
...popoverProps
}, ref) => {

// Information on the previous note, defaults to null/empty.
const prevDate = lastSavedText.date;
const prevEditor = lastSavedText.user;

return (
<Popover data-popover-category="notes" placement="bottom" {...popoverProps} ref={ref}>
crfmc marked this conversation as resolved.
Show resolved Hide resolved
<Popover.Title as="h3">Case Notes</Popover.Title>
<Popover.Content>
{
lastSavedText.date ?
<p className="last-saved small">
Last Saved: <LocalizedTime
timestamp={prevDate}
formatType="date-time-sm"
/> { prevEditor && `by ${prevEditor}` }
</p>
:
null
}
<textarea
className="form-control"
rows={5}
defaultValue={currentText}
onChange={(e) => setCurrentText(e.target.value)}
></textarea>
<button
type="button"
className="btn btn-primary mr-04 w-100"
ref={buttonRef}
onClick={() => handleNoteSave(currentText) }
disabled={lastSavedText.text === currentText ? "disabled" : "" }
>
{
// Show "Save Note" on unsaved changes OR no previous note exists
lastSavedText.date ?
lastSavedText.text === currentText ? "Note saved - edit note to save again" : "Save Note"
:
"Save Note"
}
</button>
{ lastSavedText.warning && <p className="small warning">{lastSavedText.warning}</p> }
</Popover.Content>
</Popover>
)
});

/**
* React-Boostrap (v1.6.7, Bootstrap 4.6 syntax) Overlay Trigger component.
* This component serves as the button for toggling the [CaseNotesPopover].
* Shows red indicator when [currentText] === [lastSavedText.text]
*/
const CaseNotesButton = ({
note,
lastSavedText,
handleNoteSave,
currentText,
setCurrentText,
buttonRef
}) => (
<OverlayTrigger
trigger="click"
placement="bottom"
rootClose
overlay={ // Pass Popover as overlay
<CaseNotesPopover
note={note}
lastSavedText={lastSavedText}
handleNoteSave={handleNoteSave}
currentText={currentText}
setCurrentText={setCurrentText}
buttonRef={buttonRef}
/>
}
>
<button className="case-notes-button">
<i className={`icon text-larger icon-fw icon-sticky-note ${ lastSavedText.text === "" ? 'far' : 'fas' } text-muted`}></i>
{
currentText === lastSavedText.text ?
null
:
<i
className="status-indicator-dot"
data-status={"note-dot-unsaved"}
data-tip={"Unsaved changes on this note."}
data-html
/>
}
</button>
</OverlayTrigger>
);


/**
* Top-level component for rendering the Case Notes column. Props passed down
* from columnExtensionMap.
* @param {Object} result the current search result that corresponds to this
* case (the row whose render method calls this component)
*
* Note: Items with no link to a note item (no "note" field in [result]) set
* [lastSavedText] to the empty string by default.
*/
export const CaseNotesColumn = ({ result }) => {
// Initial state passed to children
const [lastSavedText, setLastSavedText] = useState((newNote) => {

// If new note information is being provided
if (newNote) {
return {
text: newNote?.text ?? "",
date: newNote?.date ?? null,
user: newNote?.user ?? "",
userId: newNote?.userId ?? "",
warining: newNote?.warning
}
}
// If there is a note item attached to this case with deleted status
else if (result?.note?.status === "deleted") {
return {
text: "",
date: null,
user: "",
userId: "",
}
}

// Otherwise return whatever is received from [result]
return {
text: result?.note?.note_text ?? "",
date: result?.note?.last_text_edited?.date_text_edited ?? null,
user: result?.note?.last_text_edited?.text_edited_by?.display_title ?? "",
userId: result?.note?.last_text_edited?.text_edited_by?.uuid ?? "",
warning: ""
}
});

const [currentText, setCurrentText] = useState(lastSavedText.text);

// Create a ref to pass down to the "save" button
const buttonRef = useRef(null);


// Update the color of the indicator if there are unsaved changes
useEffect(() => {
// Select the status indicator HTML element using case's display title
const noteIndicator = document.querySelector(`i.status-indicator-note[data-title="${result.display_title}"]`);
noteIndicator.classList.add('icon-sticky-note')

// [currentText] is empty string (note is null or deleted), hide indicator
if (currentText === "" && currentText === lastSavedText.text) {
noteIndicator?.setAttribute('data-status', 'note-unset');
}
// [currentText] not empty, contains saved or unsaved text
else {
noteIndicator?.setAttribute('data-status', currentText === lastSavedText.text ? 'note-saved' : 'note-unsaved');
noteIndicator?.setAttribute('data-tip', currentText === lastSavedText.text ? 'Notes are available for this case.' : 'There are unsaved changes to the case notes.');
}
},[currentText, lastSavedText.text]);
crfmc marked this conversation as resolved.
Show resolved Hide resolved


const caseID = result['@id'];
const noteID = result.note ? result.note['@id'] : "";

/**
* handleNoteSave executes a request to update the NOTE and CASE
* associate with this component.
*
* If there is no note attached to this case:
* - Executes a POST request for NOTE
* - Executes a PATCH request for CASE to link to NOTE
* If there is an exisitng note:
* - Executes a PATCH request for NOTE to update the text
* If there is an existing note AND [currentText] is the empty string
* - Executes a DELETE request for the NOTE item attached
*
* Note: The save button who triggers this function is enabled iff
* [currentText] !== [lastSavedText.text]
*/
const handleNoteSave = () => {
// Replace button text with loader until the request completes (re-render)
buttonRef.current.innerHTML = `<i class="icon icon-spin icon-circle-notch fas" />`;

/**
* This the current text in the textarea input is not
* the empty string, which should be dealt with separately
*/
if (currentText !== "") {

let payload = {
"note_text": currentText,
"project": result.project['@id'],
"institution": result.institution['@id']
}

/**
* IF: the last saved text is the empty string,
* 1. POST a new note
* 2. PATCH the corresponding case to link to the new note
*/
crfmc marked this conversation as resolved.
Show resolved Hide resolved
if (noteID === "") {
// 1.POST a new note
ajax.promise("/notes-standard/", "POST", {}, JSON.stringify(payload)).then((res) => {
// Save the note item into the corresponding project
const newNoteId = res['@graph'][0]['@id'];

if (!newNoteId) {
throw new Error("No note-standard @ID returned.");
}
else {

// 2. PATCH the corresponding case to link to the new note
ajax.promise(caseID, "PATCH", {}, JSON.stringify({
"note": "" + newNoteId
})).then((patchRes) => {

if (patchRes.status === "success") {

// If the user has the same uuid (after extracting the uuid), don't change the user field
const new_userId = res['@graph'][0]?.last_text_edited.text_edited_by.split("/")[2];
let new_user = (lastSavedText.userId === new_userId) ? lastSavedText.user : "";

setLastSavedText({
text: currentText,
date: res['@graph'][0].last_text_edited.date_text_edited,
user: new_user,
userId: new_userId,
warning: "Please allow the system a few minutes to reflect these changes."
crfmc marked this conversation as resolved.
Show resolved Hide resolved
});
}
});
}
}).catch((e) => {
console.log("Error: ", e)
})
}
// ELSE: There is alrady a Note item linked, so simply modify its
// text and/or status
else {
ajax.promise(noteID, "PATCH", {}, JSON.stringify({
"note_text": currentText,
"status": "current"
})).then((patchRes) => {
if (patchRes.status === "success") {
// If the user has the same uuid (after extracting the uuid), don't change the user field
const new_userId = patchRes['@graph'][0].last_text_edited.text_edited_by.split("/")[2];
let new_user = (lastSavedText.userId === new_userId) ? lastSavedText.user : "";

setLastSavedText({
text: patchRes['@graph'][0].note_text,
date: patchRes['@graph'][0].last_text_edited.date_text_edited,
user: new_user,
userId: new_userId,
warning: "Please allow the system a few minutes to reflect these changes."
});
}
}).catch((e) => {
console.log("Error: ", e);
});
}
}
/**
* [currentText] is the empty string and we should delete the note item
* that is attached to this case item
*/
else {
/**
* Send DELETE request for the note attached, setting the status
* to "deleted", then making sure that no note is shown on rerender
*/
ajax.promise(noteID, "DELETE", {}, JSON.stringify()).then((deleteRes) => {
if (deleteRes.status === "success") {
// Replace button text with loader until the request completes (re-render)
buttonRef.current.innerHTML = `Save Note`;
setLastSavedText({
text: "",
date: "",
user: "",
userId: "",
warning: "Please allow the system a few minutes to reflect these changes."
});
}
}).catch((e) => {
console.log("Error: ", e);
});
}
}

return (
<div className="case-notes">
{/* Render Notes Button */}
<CaseNotesButton
note={result?.note}
lastSavedText={lastSavedText}
handleNoteSave={handleNoteSave}
currentText={currentText}
setCurrentText={setCurrentText}
buttonRef={buttonRef}
/>
{/* Render either empty string or note.note_text */}
crfmc marked this conversation as resolved.
Show resolved Hide resolved
<p className="case-notes-text">{lastSavedText.text}</p>
</div>
);
};
Loading
Loading