/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Button } from '@jupyter/react-components';
import type { ITranslator } from '@jupyterlab/translation';
import { nullTranslator } from '@jupyterlab/translation';
import type { ReadonlyJSONObject } from '@lumino/coreutils';
import { JSONExt } from '@lumino/coreutils';
import type { FormProps, IChangeEvent } from '@rjsf/core';
import Form from '@rjsf/core';
import type {
ArrayFieldTemplateProps,
FieldTemplateProps,
ObjectFieldTemplateProps,
Registry,
UiSchema
} from '@rjsf/utils';
import { ADDITIONAL_PROPERTY_FLAG, canExpand, getTemplate } from '@rjsf/utils';
import React from 'react';
import type { LabIcon } from '../icon';
import { addIcon, deleteIcon, moveDownIcon, moveUpIcon } from '../icon';
/**
* Default `ui:options` for the UiSchema.
*/
export const DEFAULT_UI_OPTIONS = {
/**
* This prevents the submit button from being rendered, by default, as it is
* almost never what is wanted.
*
* Provide any `uiSchema#/ui:options/submitButtonOptions` to override this.
*/
submitButtonOptions: {
norender: true
}
};
/**
* Form component namespace.
*/
export namespace FormComponent {
export interface IButtonProps {
/**
* Button style.
*/
buttonStyle?: 'icons' | 'text';
/**
* Translator for button text.
*/
translator?: ITranslator;
}
/**
* Properties for React JSON schema form's container template (array and object).
*/
export interface ILabCustomizerProps extends IButtonProps {
/**
* Whether the container is in compact mode or not.
* In compact mode the title and description are displayed more compactness.
*/
compact?: boolean;
/**
* Whether to display if the current value is not the default one.
*/
showModifiedFromDefault?: boolean;
}
/**
* Properties of the button to move an item.
*/
export interface IMoveButtonProps extends IButtonProps {
/**
* Item index to move with this button.
*/
item: ArrayFieldTemplateProps['items'][number];
/**
* Direction in which to move the item.
*/
direction: 'up' | 'down';
}
/**
* Properties of the button to drop an item.
*/
export interface IDropButtonProps extends IButtonProps {
/**
* Item index to drop with this button.
*/
item: ArrayFieldTemplateProps['items'][number];
}
/**
* Properties of the button to add an item.
*/
export interface IAddButtonProps extends IButtonProps {
/**
* Function to call to add an item.
*/
onAddClick: ArrayFieldTemplateProps['onAddClick'];
}
}
/**
* Button to move an item.
*
* @returns - the button as a react element.
*/
export const MoveButton = (
props: FormComponent.IMoveButtonProps
): JSX.Element => {
const trans = (props.translator ?? nullTranslator).load('jupyterlab');
let buttonContent: JSX.Element | string;
/**
* Whether the button is disabled or not.
*/
const disabled = () => {
if (props.direction === 'up') {
return !props.item.hasMoveUp;
} else {
return !props.item.hasMoveDown;
}
};
const moveTo =
props.direction === 'up' ? props.item.index - 1 : props.item.index + 1;
if (props.buttonStyle === 'icons') {
const iconProps: LabIcon.IReactProps = {
tag: 'span',
elementPosition: 'center'
};
buttonContent =
props.direction === 'up' ? (
) : (
);
return (
);
} else {
return (
);
}
};
/**
* Button to drop an item.
*
* @returns - the button as a react element.
*/
export const DropButton = (
props: FormComponent.IDropButtonProps
): JSX.Element => {
const trans = (props.translator ?? nullTranslator).load('jupyterlab');
let buttonContent: JSX.Element | string;
if (props.buttonStyle === 'icons') {
buttonContent = ;
return (
);
} else {
return (
);
}
};
/**
* Button to add an item.
*
* @returns - the button as a react element.
*/
export const AddButton = (
props: FormComponent.IAddButtonProps
): JSX.Element => {
const trans = (props.translator ?? nullTranslator).load('jupyterlab');
let buttonContent: JSX.Element | string;
if (props.buttonStyle === 'icons') {
buttonContent = (
);
} else {
buttonContent = trans.__('Add');
}
return (
);
};
export interface ILabCustomizerOptions
extends FormComponent.ILabCustomizerProps {
name?: string;
component: React.FunctionComponent<
P & Required
>;
}
function customizeForLab
);
}
});
/**
* Template with custom add button, necessary for accessibility and internationalization.
*/
const CustomObjectTemplateFactory = (
options: FormComponent.ILabCustomizerProps
) =>
customizeForLab({
...options,
name: 'JupyterLabObjectTemplate',
component: props => {
const { schema, registry, uiSchema, required } = props;
const commonProps = { schema, registry, uiSchema, required };
const { TitleField, DescriptionField } = getTemplates(registry, uiSchema);
return (
);
}
});
/**
* Renders the modified indicator and errors
*/
const CustomTemplateFactory = (options: FormComponent.ILabCustomizerProps) =>
customizeForLab({
...options,
name: 'JupyterLabFieldTemplate',
component: props => {
const trans = (props.translator ?? nullTranslator).load('jupyterlab');
let isModified = false;
let defaultValue: any;
const {
formData,
schema,
label,
displayLabel,
id,
formContext,
errors,
rawErrors,
children,
onKeyChange,
onDropPropertyClick
} = props;
const { defaultFormData } = formContext;
const schemaIds = id.split('_');
schemaIds.shift();
const schemaId = schemaIds.join('.');
const isRoot = schemaId === '';
const hasCustomField =
schemaId === (props.uiSchema || JSONExt.emptyObject)['ui:field'];
if (props.showModifiedFromDefault) {
/**
* Determine if the field has been modified.
* Schema Id is formatted as 'root_.'
* This logic parses out the field name to find the default value
* before determining if the field has been modified.
*/
defaultValue = schemaIds.reduce(
(acc, key) => acc?.[key],
defaultFormData
);
isModified =
!isRoot &&
formData !== undefined &&
defaultValue !== undefined &&
!schema.properties &&
schema.type !== 'array' &&
!JSONExt.deepEqual(formData, defaultValue);
}
const needsDescription =
!isRoot &&
schema.type != 'object' &&
id !=
'jp-SettingsEditor-@jupyterlab/shortcuts-extension:shortcuts_shortcuts';
// While we can implement "remove" button for array items in array template,
// object templates do not provide a way to do this instead we need to add
// buttons here (and first check if the field can be removed = is additional).
const isAdditional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
const isItem: boolean = !(
schema.type === 'object' || schema.type === 'array'
);
return (
{!hasCustomField &&
(rawErrors?.length ? (
// Shows a red indicator for fields that have validation errors
) : (
// Only show the modified indicator if there are no errors
isModified &&
))}