@datalayer/core
Version:
[](https://datalayer.io)
173 lines (172 loc) • 8.28 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
* Distributed under the terms of the Modified BSD License.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { nullTranslator } from '@jupyterlab/translation';
import { JSONExt } from '@lumino/coreutils';
import { Autocomplete, FormControl, TextInputWithTokens } from '@primer/react';
import { Box } from '@datalayer/primer-addons';
/**
* Runtime Cell Variables Picker.
*/
export function RuntimeCellVariables(props) {
const { getInputOptions, getOutputOptions, inputs, output, setInputs, setOutput, translator, } = props;
const trans = useMemo(() => (translator ?? nullTranslator).load('jupyterlab'), [translator]);
const inputForOutputRef = useRef(null);
const [willSaveOutput, setWillSaveOutput] = useState(false);
const [inputLoading, setInputLoading] = useState(!!getInputOptions);
const [outputsState, setOutputsState] = useState(null);
const [inputsState, setInputsState] = useState(inputs.map(id => ({ id, text: id, selected: true })));
const [filterVal, setFilterVal] = useState('');
// Add inputs prop in inputsState
useEffect(() => {
const candidates = inputs.filter(item => !inputsState.find(localItem => localItem.text === item));
if (candidates.length) {
setInputsState([
...inputsState,
...candidates.map(text => ({ id: text, text, selected: true })),
]);
}
}, [inputs]);
// Update inputs with selected inputsState
useEffect(() => {
const candidates = inputsState
.filter(item => item.selected)
.map(item => item.text);
if (!JSONExt.deepEqual(inputs, candidates)) {
setInputs(candidates);
}
}, [inputsState]);
// Fetch the input candidates
useEffect(() => {
if (!inputLoading) {
return;
}
if (getInputOptions) {
getInputOptions().then(ins => {
const oldItems = inputsState.map(i => i.text);
setInputsState([
...inputsState,
...ins
.sort()
.filter(item => !oldItems.includes(item))
.map(text => ({
id: text,
text,
selected: false,
})),
]);
setInputLoading(false);
});
}
else {
setInputLoading(false);
}
}, [inputsState, getInputOptions]);
// Fetch the output candidates
useEffect(() => {
if (getOutputOptions) {
Promise.all([getOutputOptions(), getInputOptions?.()])
.then(([outs, ins]) => {
setOutputsState(
// Add input variables as output candidate to cover
// mutation case
outs
.sort()
.concat((ins ?? []).sort())
// Remove duplicated variables; it may happen if
// the code introspection matches an input.
.reduce((agg, value) => {
if (!agg.includes(value)) {
agg.push(value);
}
return agg;
}, [])
.map(text => ({
id: text,
text,
selected: false,
})));
})
.catch(err => {
console.error('Failed to get the cell output candidates', err);
setOutputsState([]);
});
}
}, [getOutputOptions, getInputOptions]);
const onTokenRemove = useCallback((tokenId) => {
const idx = inputsState.findIndex(item => item.id === tokenId);
if (idx >= 0) {
inputsState.splice(idx, 1);
setInputsState([...inputsState]);
}
}, [inputsState]);
const onSelectedInputsChange = useCallback((newlySelectedItems) => {
if (!Array.isArray(newlySelectedItems)) {
newlySelectedItems = [newlySelectedItems];
}
const selectedIds = newlySelectedItems.map(i => i.id);
setInputsState([
...inputsState.map(localItem => selectedIds.includes(localItem.id)
? Object.assign(localItem, { selected: true })
: Object.assign(localItem, { selected: false })),
]);
}, [inputsState]);
const onItemSelect = useCallback((item) => {
const oldItem = inputsState.find(localItem => localItem.id === item.id);
if (!oldItem) {
setInputsState([...inputsState, item]);
}
else {
oldItem.selected = true;
setInputsState([...inputsState]);
}
}, [inputsState]);
const handleChange = useCallback((e) => {
if (e.currentTarget) {
setFilterVal(e.currentTarget.value);
}
}, []);
const onOutputChange = useCallback((e) => {
if (e.currentTarget) {
setOutput(e.currentTarget.value);
}
}, []);
const handleOutputMenuOpenChange = useCallback(
// There is a bug in primer Autocomplete that lead to the suggestion
// not triggering the `onChange` callback. So to fix it here, we
// use a pointer to the underlying input element to get the value
// and set it when the autocompletion menu closes.
(open) => {
if (!willSaveOutput) {
if (open) {
setWillSaveOutput(true);
}
return;
}
if (inputForOutputRef.current && !open) {
setOutput(inputForOutputRef.current.value);
setWillSaveOutput(false);
}
}, [willSaveOutput]);
return (_jsxs(Box, { as: "form", sx: { p: 3 }, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { id: "cell-input-variables", children: trans.__('Inputs') }), _jsxs(Autocomplete, { children: [_jsx(Autocomplete.Input, { as: TextInputWithTokens, tokens: inputsState.filter(item => item.selected), onTokenRemove: onTokenRemove, onChange: handleChange }), _jsx(Autocomplete.Overlay, { children: _jsx(Autocomplete.Menu, { addNewItem: filterVal &&
!inputsState.find(localItem => localItem.text === filterVal)
? {
id: filterVal,
text: trans.__("Add '%1'", filterVal),
handleAddItem: item => {
onItemSelect({
...item,
text: filterVal,
selected: true,
});
setFilterVal('');
},
}
: undefined, items: inputsState, selectedItemIds: inputsState
.filter(item => item.selected)
.map(item => item.id), onSelectedChange: onSelectedInputsChange, "aria-labelledby": "cell-input-variables", selectionVariant: "multiple", emptyStateText: trans.__('No available variables.'), loading: inputLoading }) })] })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { id: "cell-output-variables", children: trans.__('Output') }), _jsxs(Autocomplete, { children: [_jsx(Autocomplete.Input, { ref: inputForOutputRef, value: output, onChange: onOutputChange }), _jsx(Autocomplete.Overlay, { children: _jsx(Autocomplete.Menu, { items: outputsState ?? [], selectedItemIds: output ? [output] : [], "aria-labelledby": "cell-output-variables", loading: getOutputOptions && !outputsState, emptyStateText: trans.__('Not among the available variables.'), onOpenChange: handleOutputMenuOpenChange }) })] })] })] }));
}
export default RuntimeCellVariables;