Skip to content

Commit

Permalink
fix: DEV-3589: Add the possibility to manually add tag value in setti…
Browse files Browse the repository at this point in the history
…ngs (HumanSignal#3144)

* fix: DEV-3589: add the possibility to manually add tag value in settings

* Build frontend

* remove comments

* Build frontend

Co-authored-by: robot-ci-heartex <[email protected]>
  • Loading branch information
juliosgarbi and robot-ci-heartex authored Oct 31, 2022
1 parent 3eb3dbc commit 6bd24ca
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 40 deletions.
2 changes: 1 addition & 1 deletion label_studio/frontend/dist/react-app/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion label_studio/frontend/dist/react-app/index.js.map

Large diffs are not rendered by default.

156 changes: 118 additions & 38 deletions label_studio/frontend/src/pages/CreateProject/Config/Config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/xml/xml';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import { Button, ToggleItems } from '../../../components';
import { Form } from '../../../components/Form';
import { Oneof } from '../../../components/Oneof/Oneof';
import { cn } from '../../../utils/bem';
import { Palette } from '../../../utils/colors';
import { colorNames } from './colors';
Expand Down Expand Up @@ -202,14 +201,81 @@ const ConfigureSettings = ({ template }) => {
);
};

const ConfigureColumns = ({ columns, template }) => {
const updateValue = obj => e => {
const attrName = e.target.value.replace(/^\$/, "");
// configure value source for `obj` object tag
const ConfigureColumn = ({ template, obj, columns }) => {
const value = obj.getAttribute("value")?.replace(/^\$/, "");
// if there is a value set already and it's not in the columns
// or data was not uploaded yet
const [isManual, setIsManual] = useState(!!value && !columns?.includes(value));
// value is stored in state to make input conrollable
// changes will be sent by Enter and blur
const [newValue, setNewValue] = useState("$" + value);

// update local state when external value changes
useEffect(() => setNewValue("$" + value), [value]);

const updateValue = value => {
const newValue = value.replace(/^\$/, "");

obj.setAttribute("value", "$" + attrName);
obj.setAttribute("value", "$" + newValue);
template.render();
};

const selectValue = e => {
const value = e.target.value;

if (value === "-") {
setIsManual(true);
return;
} else if (isManual) {
setIsManual(false);
}

updateValue(value);
};

const handleChange = e => {
const newValue = e.target.value.replace(/^\$/, "");

setNewValue("$" + newValue);
};

const handleBlur = () => {
updateValue(newValue);
};

const handleKeyDown = e => {
if (e.key === "Enter") {
e.preventDefault();
updateValue(e.target.value);
}
};

return (
<p>
Use {obj.tagName.toLowerCase()}
{template.objects > 1 && ` for ${obj.getAttribute("name")}`}
{" from "}
{columns?.length > 0 && columns[0] !== DEFAULT_COLUMN && "field "}
<select onChange={selectValue} value={isManual ? "-" : value}>
{columns?.map(column => (
<option key={column} value={column}>
{column === DEFAULT_COLUMN ? "<imported file>" : `$${column}`}
</option>
))}
{!columns?.length && (
<option value={value}>{"<imported file>"}</option>
)}
<option value="-">{"<set manually>"}</option>
</select>
{isManual && (
<input value={newValue} onChange={handleChange} onBlur={handleBlur} onKeyDown={handleKeyDown}/>
)}
</p>
);
};

const ConfigureColumns = ({ columns, template }) => {
if (!template.objects.length) return null;

return (
Expand All @@ -224,35 +290,27 @@ const ConfigureColumns = ({ columns, template }) => {
</p>
)}
{template.objects.map(obj => (
<p key={obj.getAttribute("name")}>
Use {obj.tagName.toLowerCase()}
{template.objects > 1 && ` for ${obj.getAttribute("name")}`}
{" from "}
{columns?.length > 0 && columns[0] !== DEFAULT_COLUMN && "field "}
<select onChange={updateValue(obj)} value={obj.getAttribute("value")?.replace(/^\$/, "")}>
{columns?.map(column => (
<option key={column} value={column}>
{column === DEFAULT_COLUMN ? "<imported file>" : `$${column}`}
</option>
))}
{!columns?.length && (
<option value={obj.getAttribute("value")?.replace(/^\$/, "")}>{"<imported file>"}</option>
)}
</select>
</p>
<ConfigureColumn key={obj.getAttribute("name")} {...{ obj, template, columns }} />
))}
</div>
);
};

const Configurator = ({ columns, config, project, template, setTemplate, onBrowse, onSaveClick, onValidate, disableSaveButton }) => {
const Configurator = ({ columns, config, project, template, setTemplate, onBrowse, onSaveClick, onValidate, disableSaveButton, warning }) => {
const [configure, setConfigure] = React.useState(isEmptyConfig(config) ? "code" : "visual");
const [visualLoaded, loadVisual] = React.useState(configure === "visual");
const [waiting, setWaiting] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState();

// config update is debounced because of user input
const [configToCheck, setConfigToCheck] = React.useState();
// then we wait for validation and sample data for this config
const [error, setError] = React.useState();
const [parserError, setParserError] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = useState(false);
// and only with them we'll update config in preview
const [configToDisplay, setConfigToDisplay] = React.useState(config);

const debounceTimer = React.useRef();
const api = useAPI();

Expand Down Expand Up @@ -291,14 +349,14 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
setLoading(false);
if (sample && !sample.error) {
setData(sample.sample_task);
setConfigToDisplay(configToCheck);
} else {
// @todo validation can be done in this place,
// @todo but for now it's extremely slow in /sample-task endpoint
setError(sample?.response);
}
}, [configToCheck]);

React.useEffect(() => { setError(null); }, [template, config]);

// code should be reloaded on every render because of uncontrolled codemirror
// visuals should be always rendered after first render
Expand All @@ -308,12 +366,25 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
if (value === "visual") loadVisual(true);
};

const onChange = React.useCallback((config) => {
try {
setParserError(null);
setTemplate(config);
} catch(e) {
setParserError({
detail: `Parser error`,
validation_errors: [e.message],
});
}
}, [setTemplate]);

const onSave = async () => {
setError(null);
setWaiting(true);
const res = await onSaveClick();

setWaiting(false);

if (res !== true) {
setError(res);
}
Expand Down Expand Up @@ -344,7 +415,7 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
value={formatXML(config)}
detach
options={{ mode: "xml", theme: "default", lineNumbers: true }}
onChange={(editor, data, value) => setTemplate(value)}
onChange={(editor, data, value) => onChange(value)}
/>
</div>
)}
Expand All @@ -360,17 +431,26 @@ const Configurator = ({ columns, config, project, template, setTemplate, onBrows
{disableSaveButton !== true && onSaveClick && (
<Form.Actions size="small" extra={configure === "code" && extra} valid>
<Button look="primary" size="compact" style={{ width: 120 }} onClick={onSave} waiting={waiting}>
Save
{waiting ? "Saving..." : "Save"}
</Button>
</Form.Actions>
)}
</div>
<Preview config={config} data={data} error={error} loading={loading} />
<Preview config={configToDisplay} data={data} loading={loading} error={parserError || error || (configure === "code" && warning)} />
</div>
);
};

export const ConfigPage = ({ config: initialConfig = "", columns: externalColumns, project, onUpdate, onSaveClick, onValidate, disableSaveButton, show = true }) => {
export const ConfigPage = ({
config: initialConfig = "",
columns: externalColumns,
project,
onUpdate,
onSaveClick,
onValidate,
disableSaveButton,
show = true,
}) => {
const [config, _setConfig] = React.useState("");
const [mode, setMode] = React.useState("list"); // view | list
const [selectedGroup, setSelectedGroup] = React.useState(null);
Expand All @@ -395,7 +475,10 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn

React.useEffect(() => { if (externalColumns?.length) setColumns(externalColumns); }, [externalColumns]);

const [warning, setWarning] = React.useState();

React.useEffect(async () => {
if (externalColumns) return; // we are in Create Project dialog, so this request is useless
if (!project || columns) return;
const res = await api.callApi("dataSummary", {
params: { pk: project.id },
Expand All @@ -408,12 +491,6 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn
}
}, [columns, project]);

React.useEffect(() => {
if (columns?.length && template) {
template.fixColumns(columns);
}
}, [columns, template]);

const onSelectRecipe = React.useCallback(recipe => {
if (!recipe) {
setSelectedRecipe(null);
Expand Down Expand Up @@ -441,7 +518,7 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn

return (
<div className={wizardClass} data-mode="list" id="config-wizard">
<Oneof value={mode}>
{mode ==="list" && (
<TemplatesList
case="list"
selectedGroup={selectedGroup}
Expand All @@ -450,6 +527,8 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn
onSelectRecipe={onSelectRecipe}
onCustomTemplate={onCustomTemplate}
/>
)}
{mode === "view" && (
<Configurator
case="view"
columns={columns}
Expand All @@ -462,8 +541,9 @@ export const ConfigPage = ({ config: initialConfig = "", columns: externalColumn
onValidate={onValidate}
disableSaveButton={disableSaveButton}
onSaveClick={onSaveClick}
warning={warning}
/>
</Oneof>
)}
</div>
);
};

0 comments on commit 6bd24ca

Please sign in to comment.