From 3ca0c77907a87490e1f52326ea8f36ffb2b2ce99 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Mon, 16 Sep 2024 11:34:36 -0400 Subject: [PATCH 1/6] add edit edge textbox --- components/browser/EdgeBrowser.js | 53 ++++++++++++----------- components/browser/EditEdgeTextbox.js | 60 +++++++++++++++++++++++++++ components/browser/ProfileEntity.js | 22 +++++++++- lib/edge-utils.js | 3 ++ styles/pages/edge-browser.scss | 10 +++++ 5 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 components/browser/EditEdgeTextbox.js diff --git a/components/browser/EdgeBrowser.js b/components/browser/EdgeBrowser.js index 88c748c2d..1fc740086 100644 --- a/components/browser/EdgeBrowser.js +++ b/components/browser/EdgeBrowser.js @@ -306,37 +306,42 @@ export default class EdgeBrowser extends React.Component { // index is index of the column where the custom load of an entity is changed // also update if there's column with same parent updateChildColumn(index, customLoad) { - if (index + 1 >= this.state.columns.length) return - const parentIdOfColumn = this.state.columns[index].parentId - const resultColumns = [] - resultColumns.push(this.state.columns[0]) - for (let i = 1; i < this.state.columns.length; i += 1) { - const parentColumn = this.state.columns[i - 1] - const column = this.state.columns[i] - if (parentColumn.parentId === parentIdOfColumn) { - resultColumns.push({ ...column, parentCustomLoad: customLoad }) - } else { - resultColumns.push(column) + this.setState((prevState) => { + if (index + 1 >= prevState.columns.length) return null + const parentIdOfColumn = prevState.columns[index].parentId + const resultColumns = [] + resultColumns.push(prevState.columns[0]) + for (let i = 1; i < prevState.columns.length; i += 1) { + const parentColumn = prevState.columns[i - 1] + const column = prevState.columns[i] + if (parentColumn.parentId === parentIdOfColumn) { + const updatedColumn = { + ...column, + parentCustomLoad: customLoad, + } + resultColumns.push(updatedColumn) + } else { + resultColumns.push(column) + } } - } - this.setState({ columns: resultColumns }) + return { columns: resultColumns } + }) } // set the shouldUpdate property of column at index // and all other columns with same parent // to trigger entites reload of those columns reloadColumnEntities(index) { - const parentIdOfColumn = this.state.columns[index].parentId - const resultColumns = [] - for (let i = 0; i < this.state.columns.length; i += 1) { - const column = this.state.columns[i] - if (column?.parentId === parentIdOfColumn) { - resultColumns.push({ ...column, shouldReloadEntities: !column.shouldReloadEntities }) - } else { - resultColumns.push(column) - } - } - this.setState({ columns: resultColumns }) + this.setState((prevState) => { + const parentIdOfColumn = prevState.columns[index].parentId + const resultColumns = prevState.columns.map((column) => { + if (column?.parentId === parentIdOfColumn) { + return { ...column, shouldReloadEntities: !column.shouldReloadEntities } + } + return column + }) + return { columns: resultColumns } + }) } async lookupSignatures() { diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js new file mode 100644 index 000000000..97cf109b6 --- /dev/null +++ b/components/browser/EditEdgeTextbox.js @@ -0,0 +1,60 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ +/* eslint-disable react/destructuring-assignment */ +/* globals $: false */ + +import { getTooltipTitle } from '../../lib/edge-utils' + +export default function EditEdgeTextbox(props) { + const showTrashButton = props.existingEdge?.writers?.length !== 0 + + const handleHover = (target) => { + if (!props.existingEdge) return + const title = getTooltipTitle(props.existingEdge) + $(target).tooltip({ + title, + trigger: 'hover', + container: 'body', + }) + } + + if (!props.existingEdge && !props.canAddEdge) return null + return ( +
{ + e.stopPropagation() + }} + > + +
+ e.stopPropagation()} + onChange={(e) => { + e.stopPropagation() + props.addEdge({ + e, + existingEdge: props.existingEdge, + editEdgeTemplate: props.editEdgeTemplate, + updatedEdgeFields: { [props.type]: Number(e.target.value) }, + }) + }} + /> +
+ {props.existingEdge && showTrashButton && ( + { + e.stopPropagation() + props.removeEdge() + }} + > + + + )} +
+ ) +} diff --git a/components/browser/ProfileEntity.js b/components/browser/ProfileEntity.js index a6e6dd218..b6c1b6c47 100644 --- a/components/browser/ProfileEntity.js +++ b/components/browser/ProfileEntity.js @@ -21,6 +21,7 @@ import EditEdgeToggle from './EditEdgeToggle' import EditEdgeTwoDropdowns from './EditEdgeTwoDropdowns' import ScoresList from './ScoresList' import useQuery from '../../hooks/useQuery' +import EditEdgeTextbox from './EditEdgeTextbox' export default function ProfileEntity(props) { const { @@ -312,10 +313,27 @@ export default function ProfileEntity(props) { if (!edge && content?.isInvitedProfile && isEmergencyReviewerStage && !isInviteInvitation) return null + const editEdgeTextbox = (type) => ( + <> + p?.invitation === invitation.id).length === 0 || + invitation.multiReply + } + label={invitation.name} + selected={edge?.[type]} + addEdge={addEdge} + removeEdge={() => removeEdge(edge)} + type={type} // label or weight + editEdgeTemplate={editEdgeTemplates?.find((p) => p?.invitation === invitation.id)} + /> + + ) + const editEdgeDropdown = (type, controlType) => ( p?.invitation === invitation.id).length === 0 || invitation.multiReply @@ -381,12 +399,14 @@ export default function ProfileEntity(props) { const shouldRenderWeightRadio = weightRadio && !invitation.label const shouldRenderLabelDropdown = labelDropdown && !invitation.weight const shouldRenderWeightDropdown = weightDropdown && !invitation.label + const shouldRenderWeightTextbox = invitation.weight?.['value-textbox'] if (shouldRenderTwoRadio) return editEdgeTwoDropdowns('value-radio') if (shouldRenderTwoDropdown) return editEdgeTwoDropdowns('value-dropdown') if (shouldRenderLabelRadio) return editEdgeDropdown('label', 'value-radio') // for now treat radio the same as dropdown if (shouldRenderWeightRadio) return editEdgeDropdown('weight', 'value-radio') // for now treat radio the same as dropdown if (shouldRenderLabelDropdown) return editEdgeDropdown('label', 'value-dropdown') + if (shouldRenderWeightTextbox) return editEdgeTextbox('weight') if (shouldRenderWeightDropdown) return editEdgeDropdown('weight', 'value-dropdown') return editEdgeToggle() } diff --git a/lib/edge-utils.js b/lib/edge-utils.js index 8c4680a06..b570cfd83 100644 --- a/lib/edge-utils.js +++ b/lib/edge-utils.js @@ -358,6 +358,9 @@ export function translateFieldSpec(invitation, fieldName, version) { if (field.param?.enum) { spec['value-dropdown'] = field.param.enum } + if (field.param?.input === 'text') { + spec['value-textbox'] = true + } return spec } const field = invitation.reply.content[fieldName] diff --git a/styles/pages/edge-browser.scss b/styles/pages/edge-browser.scss index 581a77c04..7853bafcd 100644 --- a/styles/pages/edge-browser.scss +++ b/styles/pages/edge-browser.scss @@ -191,6 +191,16 @@ main.edge-browser { cursor: no-drop; } } + .edit-edge-textbox { + width: 15%; + padding-left: 0.5rem; + + .edit-edge-input { + font-size: 0.75rem; + padding: 3px 5px; + height: fit-content; + } + } .span-disabled { pointer-events: none; } From 517bebdac19703c5a73883ac225098d152769285 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Mon, 16 Sep 2024 19:22:29 -0400 Subject: [PATCH 2/6] fallback to default weight when delete custom max papers edge --- components/browser/EditEdgeTextbox.js | 6 +++++- components/browser/ProfileEntity.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js index 97cf109b6..cc6a4bd44 100644 --- a/components/browser/EditEdgeTextbox.js +++ b/components/browser/EditEdgeTextbox.js @@ -30,7 +30,11 @@ export default function EditEdgeTextbox(props) { e.stopPropagation()} onChange={(e) => { e.stopPropagation() diff --git a/components/browser/ProfileEntity.js b/components/browser/ProfileEntity.js index b6c1b6c47..550059eb4 100644 --- a/components/browser/ProfileEntity.js +++ b/components/browser/ProfileEntity.js @@ -139,7 +139,7 @@ export default function ProfileEntity(props) { if (isTraverseInvitation) { props.removeEdgeFromEntity(id, result) } else if (isCustomLoadInvitation) { - props.updateChildColumn(props.columnIndex, null) + props.updateChildColumn(props.columnIndex, defaultWeight) } props.reloadColumnEntities() } catch (error) { From 8261ce80930d231991fa4ad1649f3b69d0f35768 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Wed, 18 Sep 2024 12:56:29 -0400 Subject: [PATCH 3/6] delay update; add loading spinner --- components/browser/EditEdgeTextbox.js | 92 +++++++++++++++++++-------- styles/pages/edge-browser.scss | 17 +++++ 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js index cc6a4bd44..9ef8bff59 100644 --- a/components/browser/EditEdgeTextbox.js +++ b/components/browser/EditEdgeTextbox.js @@ -2,14 +2,28 @@ /* eslint-disable react/destructuring-assignment */ /* globals $: false */ +import { useCallback, useEffect, useState } from 'react' +import { debounce } from 'lodash' import { getTooltipTitle } from '../../lib/edge-utils' +import LoadingSpinner from '../LoadingSpinner' -export default function EditEdgeTextbox(props) { - const showTrashButton = props.existingEdge?.writers?.length !== 0 +const EditEdgeTextbox = ({ + existingEdge, + canAddEdge, + label, + selected, + addEdge, + removeEdge, + type, + editEdgeTemplate, +}) => { + const showTrashButton = existingEdge?.writers?.length !== 0 + const [isLoading, setIsLoading] = useState(false) + const [immediateValue, setImmediateValue] = useState(null) const handleHover = (target) => { - if (!props.existingEdge) return - const title = getTooltipTitle(props.existingEdge) + if (!existingEdge) return + const title = getTooltipTitle(existingEdge) $(target).tooltip({ title, trigger: 'hover', @@ -17,48 +31,72 @@ export default function EditEdgeTextbox(props) { }) } - if (!props.existingEdge && !props.canAddEdge) return null + const delayedAddEdge = useCallback( + debounce((e) => { + setIsLoading(true) + addEdge({ + e, + existingEdge: existingEdge, + editEdgeTemplate: editEdgeTemplate, + updatedEdgeFields: { [type]: Number(e.target.value) }, + }) + }, 500), + [existingEdge] + ) + + useEffect(() => { + setIsLoading(false) + if (selected) { + setImmediateValue(selected) + } else { + setImmediateValue(null) + } + }, [existingEdge, canAddEdge]) + + if (!existingEdge && !canAddEdge) return null return (
{ e.stopPropagation() }} > - +
e.stopPropagation()} onChange={(e) => { e.stopPropagation() - props.addEdge({ - e, - existingEdge: props.existingEdge, - editEdgeTemplate: props.editEdgeTemplate, - updatedEdgeFields: { [props.type]: Number(e.target.value) }, - }) + setImmediateValue(e.target.value) + delayedAddEdge(e) }} />
- {props.existingEdge && showTrashButton && ( - { - e.stopPropagation() - props.removeEdge() - }} - > - - - )} +
+ {isLoading && } + {existingEdge && showTrashButton && ( + { + e.stopPropagation() + setIsLoading(true) + removeEdge() + }} + > + + + )} +
) } + +export default EditEdgeTextbox diff --git a/styles/pages/edge-browser.scss b/styles/pages/edge-browser.scss index 7853bafcd..8ae30440b 100644 --- a/styles/pages/edge-browser.scss +++ b/styles/pages/edge-browser.scss @@ -199,6 +199,23 @@ main.edge-browser { font-size: 0.75rem; padding: 3px 5px; height: fit-content; + + &:disabled { + cursor: no-drop; + } + } + } + .edit-edge-spinner { + display: inline-flex; + vertical-align: middle; + + .spinner-small { + height: 100%; + display: inline-block; + width: 40px; + & > div { + background-color: constants.$orRed; + } } } .span-disabled { From 9ad75629ecf2754afb9ad5047ca86f414a990109 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Wed, 18 Sep 2024 13:02:26 -0400 Subject: [PATCH 4/6] handle 0 custom max papers --- components/browser/EditEdgeTextbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js index 9ef8bff59..a06e55d6d 100644 --- a/components/browser/EditEdgeTextbox.js +++ b/components/browser/EditEdgeTextbox.js @@ -46,7 +46,7 @@ const EditEdgeTextbox = ({ useEffect(() => { setIsLoading(false) - if (selected) { + if (selected !== null && selected !== undefined) { setImmediateValue(selected) } else { setImmediateValue(null) From b2577afbfef785afa9f91004b0e67aea252036d7 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Wed, 18 Sep 2024 13:07:16 -0400 Subject: [PATCH 5/6] fix lint --- components/browser/EditEdgeTextbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js index a06e55d6d..e9850ab87 100644 --- a/components/browser/EditEdgeTextbox.js +++ b/components/browser/EditEdgeTextbox.js @@ -36,8 +36,8 @@ const EditEdgeTextbox = ({ setIsLoading(true) addEdge({ e, - existingEdge: existingEdge, - editEdgeTemplate: editEdgeTemplate, + existingEdge, + editEdgeTemplate, updatedEdgeFields: { [type]: Number(e.target.value) }, }) }, 500), From 66d69b3ca40bfd93bd4b9e7b945909bab83a8497 Mon Sep 17 00:00:00 2001 From: xkopenreview Date: Tue, 1 Oct 2024 13:16:34 -0400 Subject: [PATCH 6/6] update error handling and button style --- components/browser/EditEdgeTextbox.js | 38 ++++++++++++++++++--------- components/browser/ProfileEntity.js | 4 ++- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/components/browser/EditEdgeTextbox.js b/components/browser/EditEdgeTextbox.js index e9850ab87..d2310d530 100644 --- a/components/browser/EditEdgeTextbox.js +++ b/components/browser/EditEdgeTextbox.js @@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from 'react' import { debounce } from 'lodash' import { getTooltipTitle } from '../../lib/edge-utils' import LoadingSpinner from '../LoadingSpinner' +import Icon from '../Icon' const EditEdgeTextbox = ({ existingEdge, @@ -32,18 +33,27 @@ const EditEdgeTextbox = ({ } const delayedAddEdge = useCallback( - debounce((e) => { + debounce(async (e) => { setIsLoading(true) - addEdge({ + const result = await addEdge({ e, existingEdge, editEdgeTemplate, updatedEdgeFields: { [type]: Number(e.target.value) }, }) + setIsLoading(false) + if (!result) setImmediateValue(selected) }, 500), [existingEdge] ) + const handleRemoveEdge = async (e) => { + e.stopPropagation() + setIsLoading(true) + await removeEdge() + setIsLoading(false) + } + useEffect(() => { setIsLoading(false) if (selected !== null && selected !== undefined) { @@ -69,7 +79,10 @@ const EditEdgeTextbox = ({ className="form-control edit-edge-input" disabled={isLoading} value={ - immediateValue ?? editEdgeTemplate?.defaultWeight ?? editEdgeTemplate?.defaultLabel + immediateValue ?? + editEdgeTemplate?.defaultWeight ?? + editEdgeTemplate?.defaultLabel ?? + '' } onClick={(e) => e.stopPropagation()} onChange={(e) => { @@ -82,17 +95,16 @@ const EditEdgeTextbox = ({
{isLoading && } {existingEdge && showTrashButton && ( - { - e.stopPropagation() - setIsLoading(true) - removeEdge() - }} + )}
diff --git a/components/browser/ProfileEntity.js b/components/browser/ProfileEntity.js index 550059eb4..e08ceeb3f 100644 --- a/components/browser/ProfileEntity.js +++ b/components/browser/ProfileEntity.js @@ -200,7 +200,7 @@ export default function ProfileEntity(props) { ) if (version === 1 && (!signatures || signatures.length === 0)) { promptError("You don't have permission to edit this edge") - return + return false } const { @@ -237,8 +237,10 @@ export default function ProfileEntity(props) { promptMessage( `Invitation has been sent to ${body.tail} and it's waiting for the response.` ) + return true } catch (error) { promptError(error.message) + return false } }