Skip to content

Commit

Permalink
[1426] Add completion support to TextfieldPropertySection
Browse files Browse the repository at this point in the history
Bug: #1426
Signed-off-by: Pierre-Charles David <pierre-charles.david@obeo.fr>
  • Loading branch information
pcdavid committed Oct 27, 2022
1 parent 118035e commit 81b2426
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { gql, useMutation } from '@apollo/client';
import { gql, useLazyQuery, useMutation } from '@apollo/client';
import IconButton from '@material-ui/core/IconButton';
import List from '@material-ui/core/List';
import Paper from '@material-ui/core/Paper';
import Snackbar from '@material-ui/core/Snackbar';
import { makeStyles, Theme } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import { useMachine } from '@xstate/react';
import React, { useEffect } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { GQLTextarea, GQLWidget } from '../form/FormEventFragments.types';
import { getTextDecorationLineValue } from './getTextDecorationLineValue';
import { PropertySectionLabel } from './PropertySectionLabel';
import {
GQLCompletionProposal,
GQLEditTextfieldInput,
GQLEditTextfieldMutationData,
GQLEditTextfieldMutationVariables,
Expand Down Expand Up @@ -52,9 +57,10 @@ export interface StyleProps {
bold: boolean | null;
underline: boolean | null;
strikeThrough: boolean | null;
popupOffset: number | null;
}

const useStyle = makeStyles<Theme, StyleProps>(() => ({
const useStyle = makeStyles<Theme, StyleProps>((theme) => ({
style: {
backgroundColor: ({ backgroundColor }) => (backgroundColor ? backgroundColor : 'inherit'),
color: ({ foregroundColor }) => (foregroundColor ? foregroundColor : 'inherit'),
Expand All @@ -63,8 +69,36 @@ const useStyle = makeStyles<Theme, StyleProps>(() => ({
fontWeight: ({ bold }) => (bold ? 'bold' : 'inherit'),
textDecorationLine: ({ underline, strikeThrough }) => getTextDecorationLineValue(underline, strikeThrough),
},
popup: {
position: 'fixed',
zIndex: theme.zIndex.snackbar,
top: ({ popupOffset }) => (popupOffset ? popupOffset + theme.spacing(1) : 0),
width: 'max(100%, 20vw)',
},
}));

export const getCompletionProposalsQuery = gql`
query completionProposals($editingContextId: ID!, $input: CompletionRequestInput!) {
viewer {
editingContext(editingContextId: $editingContextId) {
completionProposals(input: $input) {
__typename
... on CompletionRequestSuccessPayload {
proposals {
description
textToInsert
charsToReplace
}
}
... on ErrorPayload {
message
}
}
}
}
}
`;

export const editTextfieldMutation = gql`
mutation editTextfield($input: EditTextfieldInput!) {
editTextfield(input: $input) {
Expand Down Expand Up @@ -102,6 +136,8 @@ export const TextfieldPropertySection = ({
subscribers,
readOnly,
}: TextfieldPropertySectionProps) => {
const inputElt = useRef<HTMLInputElement>();

const props: StyleProps = {
backgroundColor: widget.style?.backgroundColor ?? null,
foregroundColor: widget.style?.foregroundColor ?? null,
Expand All @@ -110,6 +146,7 @@ export const TextfieldPropertySection = ({
bold: widget.style?.bold ?? null,
underline: widget.style?.underline ?? null,
strikeThrough: widget.style?.strikeThrough ?? null,
popupOffset: inputElt?.current?.getBoundingClientRect()?.bottom ?? null,
};
const classes = useStyle(props);

Expand Down Expand Up @@ -221,41 +258,127 @@ export const TextfieldPropertySection = ({
sendEditedValue();
};

const [proposals, setProposals] = useState<GQLCompletionProposal[] | null>(null);

const [getCompletionProposals, { loading: proposalsLoading, data: proposalsData, error: proposalsError }] =
useLazyQuery(getCompletionProposalsQuery);
useEffect(() => {
if (!proposalsLoading) {
if (proposalsError) {
const message = proposalsError.message;
const showToastEvent: ShowToastEvent = { type: 'SHOW_TOAST', message };
dispatch(showToastEvent);
}
if (proposalsData) {
const proposalsReceived: GQLCompletionProposal[] =
proposalsData?.viewer?.editingContext?.completionProposals?.proposals;
setProposals(proposalsReceived);
console.table(proposalsReceived);
if (proposalsReceived && proposalsReceived.length === 1) {
applyProposal(proposalsReceived[0]);
}
}
}
}, [proposalsLoading, proposalsData, proposalsError]);

const [offset, setOffset] = useState<number>(0);

const applyProposal = (proposal) => {
const newValue =
value.substring(0, offset) + proposal.textToInsert + value.substring(offset + proposal.charsToReplace);
const changeValueEvent: ChangeValueEvent = { type: 'CHANGE_VALUE', value: newValue };
dispatch(changeValueEvent);
};

const onKeyPress: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
if ('Enter' === event.key && !event.shiftKey) {
event.preventDefault();
sendEditedValue();
}
setProposals(null);
};

const [controlDown, setControlDown] = useState<boolean>(false);
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
if ('Control' === event.key) {
setControlDown(true);
} else if ('Escape' === event.key) {
setProposals(null);
}
if (widget.supportsCompletion && ' ' === event.key && controlDown) {
setOffset((event.target as HTMLInputElement).selectionStart);
console.log(`Requested completion for '${value}' at `, offset);
getCompletionProposals({
variables: {
editingContextId,
input: {
id: uuid(),
editingContextId,
representationId: formId,
widgetId: widget.id,
currentText: value,
cursorPosition: (event.target as HTMLInputElement).selectionStart,
},
},
});
}
};
const onKeyUp: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
if ('Control' === event.key) {
setControlDown(false);
}
};

let popup = null;
if (proposals) {
popup = (
<Paper elevation={3} className={classes.popup}>
<List>
{proposals.slice(0, 10).map((proposal, index) => (
<Tooltip key={index} title={proposal.description}>
<Typography onClick={() => applyProposal(proposal)}>{proposal.textToInsert}</Typography>
</Tooltip>
))}
</List>
</Paper>
);
}

return (
<div>
<PropertySectionLabel label={widget.label} subscribers={subscribers} />
<TextField
name={widget.label}
placeholder={widget.label}
value={value}
spellCheck={false}
margin="dense"
multiline={isTextarea(widget)}
maxRows={isTextarea(widget) ? 4 : 1}
fullWidth
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
onKeyPress={onKeyPress}
data-testid={widget.label}
disabled={readOnly}
error={widget.diagnostics.length > 0}
helperText={widget.diagnostics[0]?.message}
InputProps={
widget.style
? {
className: classes.style,
}
: {}
}
/>
<>
<TextField
name={widget.label}
placeholder={widget.label}
value={value}
spellCheck={false}
margin="dense"
multiline={isTextarea(widget)}
maxRows={isTextarea(widget) ? 4 : 1}
fullWidth
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
onKeyPress={onKeyPress}
data-testid={widget.label}
disabled={readOnly}
error={widget.diagnostics.length > 0}
helperText={widget.diagnostics[0]?.message}
inputRef={inputElt}
InputProps={
widget.style
? {
className: classes.style,
}
: {}
}
/>
{popup}
</>

<Snackbar
anchorOrigin={{
vertical: 'bottom',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ export interface GQLUpdateWidgetFocusPayload {
}

export interface GQLUpdateWidgetFocusSuccessPayload extends GQLUpdateWidgetFocusPayload {}

export interface GQLCompletionProposal {
description: string;
textToInsert: string;
charsToReplace: number;
}

0 comments on commit 81b2426

Please sign in to comment.