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

Support Path Autocompletion in Editors #794

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
14 changes: 12 additions & 2 deletions workspace/src/app/views/shared/RuleEditor/RuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import utils from "./RuleEditor.utils";
import { IStickyNote } from "views/taskViews/shared/task.typings";
import { DatasetCharacteristics } from "../typings";
import { ReactFlowHotkeyContext } from "@eccenca/gui-elements/src/cmem/react-flow/extensions/ReactFlowHotkeyContext";
import {Notification} from "@eccenca/gui-elements"
import {diErrorMessage} from "@ducks/error/typings";
import { Notification } from "@eccenca/gui-elements";
import { diErrorMessage } from "@ducks/error/typings";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

/** Function to fetch the rule operator spec. */
export type RuleOperatorFetchFnType = (
Expand Down Expand Up @@ -89,6 +90,13 @@ export interface RuleEditorProps<RULE_TYPE, OPERATOR_TYPE> {
) => Map<string, DatasetCharacteristics> | Promise<Map<string, DatasetCharacteristics>>;
/** Returns for a path input plugin and a path the type of the given path. Returns undefined if either the plugin does not exist or the path data is unknown. */
inputPathPluginPathType?: (inputPathPluginId: string, path: string) => string | undefined;
/**
* Fetches partial auto-completion results for the transforms task input paths, i.e. any part of a path could be auto-completed
* without replacing the complete path.
*/
partialAutoCompletion: (
inputType: "source" | "target"
) => (inputString: string, cursorPosition: number) => Promise<IPartialAutoCompleteResult | undefined>;
}

const READ_ONLY_QUERY_PARAMETER = "readOnly";
Expand Down Expand Up @@ -118,6 +126,7 @@ const RuleEditor = <TASK_TYPE extends object, OPERATOR_TYPE extends object>({
instanceId,
fetchDatasetCharacteristics,
inputPathPluginPathType,
partialAutoCompletion,
}: RuleEditorProps<TASK_TYPE, OPERATOR_TYPE>) => {
// The task that contains the rule, e.g. transform or linking task
const [taskData, setTaskData] = React.useState<TASK_TYPE | undefined>(undefined);
Expand Down Expand Up @@ -278,6 +287,7 @@ const RuleEditor = <TASK_TYPE extends object, OPERATOR_TYPE extends object>({
instanceId,
datasetCharacteristics,
inputPathPluginPathType,
partialAutoCompletion,
}}
>
<ReactFlowHotkeyContext.Provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { IViewActions } from "../../../plugins/PluginRegistry";
import { IStickyNote } from "views/taskViews/shared/task.typings";
import { DatasetCharacteristics } from "../../typings";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

/**
* The rule editor context that contains objects and methods related to the original objects that are being edited and
Expand Down Expand Up @@ -77,6 +78,13 @@ export interface RuleEditorContextProps {
datasetCharacteristics: Map<string, DatasetCharacteristics>;
/** Returns for a path input plugin and a path the type of the given path. Returns undefined if either the plugin does not exist or the path data is unknown. */
inputPathPluginPathType?: (inputPathPluginId: string, path: string) => string | undefined;
/**
* Fetches partial auto-completion results for the transforms task input paths, i.e. any part of a path could be auto-completed
* without replacing the complete path.
*/
partialAutoCompletion: (
inputType: "source" | "target"
) => (inputString: string, cursorPosition: number) => Promise<IPartialAutoCompleteResult | undefined>;
}

/** Creates a rule editor model context that contains the actual rule model and low-level update functions. */
Expand All @@ -99,4 +107,5 @@ export const RuleEditorContext = React.createContext<RuleEditorContextProps>({
instanceId: "uniqueId",
datasetCharacteristics: new Map(),
inputPathPluginPathType: () => undefined,
partialAutoCompletion: () => async () => undefined,
});
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe("Rule editor model", () => {
validateConnection,
instanceId: "id",
datasetCharacteristics: new Map(),
partialAutoCompletion: () => async () => undefined,
}}
>
<ReactFlowProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {
} from "../../../modals/CreateArtefactModal/ArtefactForms/ParameterAutoCompletion";
import React from "react";
import { IAutocompleteDefaultResponse } from "@ducks/shared/typings";
import { Button, IconButton, MenuItem, Select } from "@eccenca/gui-elements";
import { Button, CodeAutocompleteField, IconButton, MenuItem, Select } from "@eccenca/gui-elements";
import { useTranslation } from "react-i18next";
import { checkValuePathValidity } from "../../../../../views/pages/MappingEditor/HierarchicalMapping/store";

/** Language filter related properties. */
export interface LanguageFilterProps {
Expand Down Expand Up @@ -194,7 +195,29 @@ export const PathInputOperator = ({
setActiveProps(newProps);
}

return <ParameterAutoCompletion {...activeProps} {...overwrittenProps} showErrorsInline={true} />;
const autoCompletionInput = React.useMemo(() => {
const inputType = activeProps.pluginId.replace("PathInput", "") as "source" | "target";
const fetchSuggestion =
(activeProps.partialAutoCompletion && activeProps.partialAutoCompletion(inputType)) ||
(async () => undefined);
return (
<CodeAutocompleteField
{...overwrittenProps.inputProps}
initialValue={activeProps.initialValue?.value ?? ""}
onChange={(value) => activeProps.onChange({ value })}
fetchSuggestions={fetchSuggestion}
placeholder={t("ActiveLearning.config.manualSelection.insertPath")}
checkInput={(value) => checkValuePathValidity(value, activeProps.projectId)}
validationErrorText={t("ActiveLearning.config.errors.invalidPath")}
autoCompletionRequestDelay={500}
validationRequestDelay={250}
/>
);
}, [activeProps, languageFilter, showLanguageFilterButton]);

return autoCompletionInput;

// return <ParameterAutoCompletion {...activeProps} {...overwrittenProps} showErrorsInline={true} />;
};

const languageFilterExpression = (lang: string | undefined) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const RuleParameterInput = ({
autoComplete
) => ({
projectId: ruleEditorContext.projectId,
taskId: ruleEditorContext.editedItemId!,
paramId: ruleParameter.parameterId,
pluginId: pluginId,
onChange: inputAttributes.onChange,
Expand All @@ -106,6 +107,7 @@ export const RuleParameterInput = ({
required: ruleParameter.parameterSpecification.required,
readOnly: inputAttributes.readOnly,
hasBackDrop: !insideModal,
partialAutoCompletion: ruleEditorContext.partialAutoCompletion,
});

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { RegisterForExternalChangesFn } from "./InputMapper";
import { InputGroupProps as BlueprintInputGroupProps } from "@blueprintjs/core/lib/esm/components/forms/inputGroup";
import { HTMLInputProps as BlueprintHTMLInputProps } from "@blueprintjs/core/lib/esm/common/props";
import { CreateArtefactModalContext } from "../CreateArtefactModalContext";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

export interface ParameterAutoCompletionProps {
/** ID of the parameter. */
Expand Down Expand Up @@ -58,6 +59,13 @@ export interface ParameterAutoCompletionProps {
* are maintained by this component.
*/
inputProps?: BlueprintInputGroupProps & BlueprintHTMLInputProps;
/**
* Fetches partial auto-completion results for the transforms task input paths, i.e. any part of a path could be auto-completed
* without replacing the complete path.
*/
partialAutoCompletion?: (
inputType: "source" | "target"
) => (inputString: string, cursorPosition: number) => Promise<IPartialAutoCompleteResult | undefined>;
}

type StringOrReifiedValue = IAutocompleteDefaultResponse | string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fetch from "../../../services/fetch";
import { legacyLinkingEndpoint } from "../../../utils/getApiEndpoint";
import { legacyLinkingEndpoint, legacyTransformEndpoint } from "../../../utils/getApiEndpoint";
import { FetchResponse } from "../../../services/fetch/responseInterceptor";
import { PathWithMetaData } from "../shared/rules/rule.typings";
import { IEntityLink, IEvaluatedReferenceLinks, ILinkingRule, ILinkingTaskParameters } from "./linking.types";
Expand Down
32 changes: 31 additions & 1 deletion workspace/src/app/views/taskViews/linking/LinkingRuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
} from "../../shared/RuleEditor/RuleEditor.typings";
import { useSelector } from "react-redux";
import { commonSel } from "@ducks/common";
import linkingRuleRequests, { fetchLinkSpec, updateLinkageRule } from "./LinkingRuleEditor.requests";
import linkingRuleRequests, {
fetchLinkSpec,
partialAutoCompleteLinkingInputPaths,
updateLinkageRule,
} from "./LinkingRuleEditor.requests";
import { PathWithMetaData } from "../shared/rules/rule.typings";
import { IAutocompleteDefaultResponse, TaskPlugin } from "@ducks/shared/typings";
import { FetchError, FetchResponse } from "../../../services/fetch/responseInterceptor";
Expand All @@ -33,6 +37,7 @@ import {
import { invalidValueResult } from "../../../views/shared/RuleEditor/view/ruleNode/ruleNode.utils";
import { diErrorMessage } from "@ducks/error/typings";
import { Notification, highlighterUtils } from "@eccenca/gui-elements";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

export interface LinkingRuleEditorProps {
/** Project ID the task is in. */
Expand Down Expand Up @@ -290,6 +295,30 @@ export const LinkingRuleEditor = ({ projectId, linkingTaskId, viewActions, insta
});
};

const fetchPartialAutoCompletionResult = React.useCallback(
(inputType: "source" | "target") =>
async (inputString: string, cursorPosition: number): Promise<IPartialAutoCompleteResult | undefined> => {
try {
const result = await partialAutoCompleteLinkingInputPaths(
projectId,
linkingTaskId,
inputType,
inputString,
cursorPosition,
200
);
return result.data;
} catch (err) {
registerError(
"LinkingRuleEditor_partialAutoCompletion",
t("taskViews.linkRulesEditor.errors.partialPathAutoCompletion.msg"),
err
);
}
},
[]
);

const sourcePathInput = () =>
ruleUtils.inputPathOperator(
"sourcePathInput",
Expand Down Expand Up @@ -386,6 +415,7 @@ export const LinkingRuleEditor = ({ projectId, linkingTaskId, viewActions, insta
taskId={linkingTaskId}
fetchRuleData={fetchTaskData}
fetchRuleOperators={fetchLinkingRuleOperatorDetails}
partialAutoCompletion={fetchPartialAutoCompletionResult}
saveRule={saveLinkageRule}
getStickyNotes={utils.getStickyNotes}
convertRuleOperator={ruleUtils.convertRuleOperator}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { IViewActions } from "../../plugins/PluginRegistry";
import RuleEditor, { RuleOperatorFetchFnType } from "../../shared/RuleEditor/RuleEditor";
import { requestRuleOperatorPluginsDetails } from "@ducks/common/requests";
import { IPluginDetails } from "@ducks/common/typings";
import { autoCompleteTransformSourcePath, putTransformRule, requestTransformRule } from "./transform.requests";
import {
autoCompleteTransformSourcePath,
partialAutoCompleteTransformInputPaths,
putTransformRule,
requestTransformRule,
} from "./transform.requests";
import {
IRuleOperatorNode,
RuleSaveNodeError,
Expand All @@ -23,6 +28,7 @@ import TransformRuleEvaluation from "./evaluation/TransformRuleEvaluation";
import { DatasetCharacteristics } from "../../shared/typings";
import { requestDatasetCharacteristics, requestTaskData } from "@ducks/shared/requests";
import { GlobalMappingEditorContext } from "../../pages/MappingEditor/contexts/GlobalMappingEditorContext";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

export interface TransformRuleEditorProps {
/** Project ID the task is in. */
Expand Down Expand Up @@ -196,6 +202,30 @@ export const TransformRuleEditor = ({
}
};

const fetchPartialAutoCompletionResult = React.useCallback(
() =>
async (inputString: string, cursorPosition: number): Promise<IPartialAutoCompleteResult | undefined> => {
try {
const result = await partialAutoCompleteTransformInputPaths(
projectId,
transformTaskId,
containerRuleId,
inputString,
cursorPosition,
200
);
return result.data;
} catch (err) {
registerError(
"TransformRuleEditor_partialAutoCompletion",
t("taskViews.transformRulesEditor.errors.partialPathAutoCompletion.msg"),
err
);
}
},
[]
);

const sourcePathInput = () =>
ruleUtils.inputPathOperator(
"sourcePathInput",
Expand Down Expand Up @@ -260,6 +290,7 @@ export const TransformRuleEditor = ({
saveRule={saveTransformRule}
convertRuleOperator={ruleUtils.convertRuleOperator}
convertToRuleOperatorNodes={convertToRuleOperatorNodes}
partialAutoCompletion={fetchPartialAutoCompletionResult}
viewActions={viewActions}
additionalToolBarComponents={additionalToolBarComponents}
getStickyNotes={getStickyNotes}
Expand Down
42 changes: 38 additions & 4 deletions workspace/src/app/views/taskViews/transform/transform.requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import fetch from "../../../services/fetch";
import { legacyTransformEndpoint } from "../../../utils/getApiEndpoint";
import { IComplexMappingRule, ITransformRule } from "./transform.types";
import { IAutocompleteDefaultResponse } from "@ducks/shared/typings";
import {TaskContext} from "../../shared/projectTaskTabView/projectTaskTabView.typing";
import { TaskContext } from "../../shared/projectTaskTabView/projectTaskTabView.typing";
import { IPartialAutoCompleteResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion";

/** Fetches a transform rule. */
export const requestTransformRule = async (
Expand All @@ -19,13 +20,46 @@ export const requestTransformRule = async (
});
};

/**
* Fetches partial auto-completion results for the transforms task input paths, i.e. any part of a path could be auto-completed
* without replacing the complete path.
*
* @param projectId
* @param transformTaskId
* @param rule
* @param inputType Fetches paths either from the source or target input data source.
* @param inputString The path input string
* @param cursorPosition The cursor position inside the input string
* @param limit The max number of results to return.
*/
export const partialAutoCompleteTransformInputPaths = (
projectId: string,
transformTaskId: string,
rule: string,
inputString: string,
cursorPosition: number,
limit?: number
): Promise<FetchResponse<IPartialAutoCompleteResult>> => {
return fetch({
url: legacyTransformEndpoint(
`/tasks/${projectId}/${transformTaskId}/rule/${rule}/completions/partialSourcePaths`
),
method: "POST",
body: {
inputString,
cursorPosition,
maxSuggestions: limit,
},
});
};

/** fetch source paths for transform editor */
export const autoCompleteTransformSourcePath = (
projectId: string,
taskId: string,
ruleId: string,
term = "",
taskContext?: TaskContext,
taskContext?: TaskContext,
limit = 100
): Promise<FetchResponse<IAutocompleteDefaultResponse[]>> => {
return fetch({
Expand All @@ -34,8 +68,8 @@ export const autoCompleteTransformSourcePath = (
),
method: "POST",
body: {
taskContext
}
taskContext,
},
});
};

Expand Down
6 changes: 6 additions & 0 deletions workspace/src/locales/manual/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,9 @@
"taskViews": {
"linkRulesEditor": {
"errors": {
"partialPathAutoCompletion": {
"msg": "Could not fetch partial autocompletion"
},
"fetchTaskData": {
"msg": "An error has occurred while fetching the linking task."
},
Expand Down Expand Up @@ -752,6 +755,9 @@
}
},
"errors": {
"partialPathAutoCompletion": {
"msg": "Could not fetch partial autocompletion"
},
"fetchTransformRule": {
"msg": "An error has occurred while fetching the transform rule."
},
Expand Down