diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index fec1003c1..6f1add0f2 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -289,7 +289,7 @@ export function addCommands( createSource: true, sourceData: { name: 'Custom Image Source', - url: 'https://maplibre.org/maplibre-gl-js/docs/assets/radar.gif', + path: 'https://maplibre.org/maplibre-gl-js/docs/assets/radar.gif', coordinates: [ [-80.425, 46.437], [-71.516, 46.437], diff --git a/packages/base/src/dialogs/formdialog.tsx b/packages/base/src/dialogs/formdialog.tsx index 0d8c4bb6f..d6631d39b 100644 --- a/packages/base/src/dialogs/formdialog.tsx +++ b/packages/base/src/dialogs/formdialog.tsx @@ -18,6 +18,11 @@ export interface ICreationFormWrapperProps extends ICreationFormProps { * some extra errors or not. */ formErrorSignalPromise?: PromiseDelegate, boolean>>; + /** + * Configuration options for the dialog, including settings for layer data, source data, + * and other form-related parameters. + */ + dialogOptions?: any; } export interface ICreationFormDialogOptions extends ICreationFormProps { @@ -53,6 +58,7 @@ export const CreationFormWrapper = (props: ICreationFormWrapperProps) => { ok={okSignal.current} cancel={props.cancel} formErrorSignal={formErrorSignal.current} + dialogOptions={props.dialogOptions} /> ) ); @@ -88,6 +94,7 @@ export class CreationFormDialog extends Dialog { okSignalPromise={okSignalPromise} cancel={cancelCallback} formErrorSignalPromise={formErrorSignalPromise} + dialogOptions={options} /> ); diff --git a/packages/base/src/formbuilder/creationform.tsx b/packages/base/src/formbuilder/creationform.tsx index bb012d1b7..959a79b44 100644 --- a/packages/base/src/formbuilder/creationform.tsx +++ b/packages/base/src/formbuilder/creationform.tsx @@ -67,6 +67,12 @@ export interface ICreationFormProps { * extra errors or not. */ formErrorSignal?: Signal, boolean>; + + /** + * Configuration options for the dialog, including settings for layer data, source data, + * and other form-related parameters. + */ + dialogOptions?: any; } /** @@ -206,6 +212,7 @@ export class CreationForm extends React.Component { cancel={this.props.cancel} formChangedSignal={this.sourceFormChangedSignal} formErrorSignal={this.props.formErrorSignal} + dialogOptions={this.props.dialogOptions} /> )} @@ -226,6 +233,7 @@ export class CreationForm extends React.Component { cancel={this.props.cancel} sourceFormChangedSignal={this.sourceFormChangedSignal} formErrorSignal={this.props.formErrorSignal} + dialogOptions={this.props.dialogOptions} /> )} diff --git a/packages/base/src/formbuilder/editform.tsx b/packages/base/src/formbuilder/editform.tsx index 3a2c3aaa8..6cc2c9947 100644 --- a/packages/base/src/formbuilder/editform.tsx +++ b/packages/base/src/formbuilder/editform.tsx @@ -13,6 +13,7 @@ import * as React from 'react'; import { getLayerTypeForm, getSourceTypeForm } from './formselectors'; import { LayerPropertiesForm } from './objectform/layerform'; import { BaseForm } from './objectform/baseform'; +import { Signal } from '@lumino/signaling'; export interface IEditFormProps { /** @@ -118,10 +119,13 @@ export class EditForm extends React.Component { syncData={(properties: { [key: string]: any }) => { this.syncObjectProperties(this.props.source, properties); }} + formChangedSignal={this.sourceFormChangedSignal} /> )} ); } + private sourceFormChangedSignal: Signal, IDict> = + new Signal(this); } diff --git a/packages/base/src/formbuilder/objectform/baseform.tsx b/packages/base/src/formbuilder/objectform/baseform.tsx index 50fbda2f6..fbd24e623 100644 --- a/packages/base/src/formbuilder/objectform/baseform.tsx +++ b/packages/base/src/formbuilder/objectform/baseform.tsx @@ -8,6 +8,7 @@ import { Signal } from '@lumino/signaling'; import { deepCopy } from '../../tools'; import { IDict } from '../../types'; import { Slider, SliderLabel } from '@jupyter/react-components'; +import { FileSelectorWidget } from './fileselectorwidget'; export interface IBaseFormStates { schema?: IDict; @@ -66,6 +67,12 @@ export interface IBaseFormProps { * extra errors or not. */ formErrorSignal?: Signal, boolean>; + + /** + * Configuration options for the dialog, including settings for layer data, source data, + * and other form-related parameters. + */ + dialogOptions?: any; } const WrappedFormComponent = (props: any): JSX.Element => { @@ -195,6 +202,20 @@ export class BaseForm extends React.Component { this.removeFormEntry(k, data, schema, uiSchema); } } + + // Customize the widget for path field + if (schema.properties && schema.properties.path) { + const docManager = + this.props.formChangedSignal?.sender.props.formSchemaRegistry.getDocManager(); + + uiSchema.path = { + 'ui:widget': FileSelectorWidget, + 'ui:options': { + docManager, + formOptions: this.props + } + }; + } }); } diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx new file mode 100644 index 000000000..d152027ac --- /dev/null +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -0,0 +1,123 @@ +import React, { useState, useEffect } from 'react'; +import { FileDialog } from '@jupyterlab/filebrowser'; +import { Dialog } from '@jupyterlab/apputils'; +import { CreationFormDialog } from '../../dialogs/formdialog'; +import { PathExt } from '@jupyterlab/coreutils'; + +export const FileSelectorWidget = (props: any) => { + const { options } = props; + const { docManager, formOptions } = options; + + const [serverFilePath, setServerFilePath] = useState(''); + const [urlPath, setUrlPath] = useState(''); + + useEffect(() => { + if (props.value) { + if ( + props.value.startsWith('http://') || + props.value.startsWith('https://') + ) { + setUrlPath(props.value); + setServerFilePath(''); + } else { + setServerFilePath(props.value); + setUrlPath(''); + } + } + }, [props.value]); + + const handleBrowseServerFiles = async () => { + try { + const dialogElement = document.querySelector( + 'dialog[aria-modal="true"]' + ) as HTMLDialogElement; + if (dialogElement) { + const dialogInstance = Dialog.tracker.find( + dialog => dialog.node === dialogElement + ); + + if (dialogInstance) { + dialogInstance.resolve(0); + } + } else { + console.warn('No open dialog found.'); + } + + const output = await FileDialog.getOpenFiles({ + title: 'Select a File', + manager: docManager + }); + + if (output.value && output.value.length > 0) { + const selectedFilePath = output.value[0].path; + + const relativePath = PathExt.relative( + formOptions.filePath, + selectedFilePath + ); + + setServerFilePath(relativePath); + setUrlPath(''); + props.onChange(relativePath); + + if (dialogElement) { + formOptions.dialogOptions.sourceData = { + ...formOptions.sourceData, + path: relativePath + }; + + const formDialog = new CreationFormDialog({ + ...formOptions.dialogOptions + }); + await formDialog.launch(); + } + } else { + if (dialogElement) { + const formDialog = new CreationFormDialog({ + ...formOptions.dialogOptions + }); + await formDialog.launch(); + } + } + } catch (e) { + console.error('Error handling file dialog:', e); + } + }; + + const handleURLChange = (event: React.ChangeEvent) => { + const url = event.target.value; + setServerFilePath(''); + setUrlPath(url); + props.onChange(url); + }; + + return ( +
+
+ + +
+
+

+ Or enter external URL +

+ +
+
+ ); +}; diff --git a/packages/base/src/formbuilder/objectform/geojsonsource.ts b/packages/base/src/formbuilder/objectform/geojsonsource.ts index 22722e62e..7526190e2 100644 --- a/packages/base/src/formbuilder/objectform/geojsonsource.ts +++ b/packages/base/src/formbuilder/objectform/geojsonsource.ts @@ -1,6 +1,6 @@ import { IDict } from '@jupytergis/schema'; import { showErrorMessage } from '@jupyterlab/apputils'; -import { ISubmitEvent } from '@rjsf/core'; +import { IChangeEvent, ISubmitEvent } from '@rjsf/core'; import { Ajv, ValidateFunction } from 'ajv'; import * as geojson from '@jupytergis/schema/src/schema/geojson.json'; @@ -45,6 +45,14 @@ export class GeoJSONSourcePropertiesForm extends BaseForm { this._validatePath(value); } + // we need to use `onFormChange` instead of `onFormBlur` because it's no longer a text field + protected onFormChange(e: IChangeEvent): void { + super.onFormChange(e); + if (e.formData?.path) { + this._validatePath(e.formData.path); + } + } + protected onFormSubmit(e: ISubmitEvent) { if (this.state.extraErrors?.path?.__errors?.length >= 1) { showErrorMessage( diff --git a/packages/base/src/formbuilder/objectform/layerform.ts b/packages/base/src/formbuilder/objectform/layerform.ts index 73702fe8f..5b4acbf00 100644 --- a/packages/base/src/formbuilder/objectform/layerform.ts +++ b/packages/base/src/formbuilder/objectform/layerform.ts @@ -1,6 +1,7 @@ import { IDict, SourceType } from '@jupytergis/schema'; import { BaseForm, IBaseFormProps } from './baseform'; import { Signal } from '@lumino/signaling'; +import { IChangeEvent } from '@rjsf/core'; export interface ILayerProps extends IBaseFormProps { /** @@ -43,4 +44,11 @@ export class LayerPropertiesForm extends BaseForm { schema.properties.source.enumNames = Object.values(availableSources); schema.properties.source.enum = Object.keys(availableSources); } + + protected onFormChange(e: IChangeEvent): void { + super.onFormChange(e); + if (this.props.dialogOptions) { + this.props.dialogOptions.layerData = { ...e.formData }; + } + } } diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 134cd3f37..e3e25340b 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -578,7 +578,7 @@ export class MainView extends React.Component { const extent = [minX, minY, maxX, maxY]; const imageUrl = await loadFile({ - filepath: sourceParameters.url, + filepath: sourceParameters.path, type: 'ImageSource', model: this._model }); diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 53cf29490..c92b91c27 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -11,6 +11,7 @@ import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry'; import { Contents, User } from '@jupyterlab/services'; import { ISignal, Signal } from '@lumino/signaling'; import { SplitPanel } from '@lumino/widgets'; +import { IDocumentManager } from '@jupyterlab/docmanager'; import { IJGISContent, @@ -246,6 +247,8 @@ export interface IJGISFormSchemaRegistry { * @memberof IJGISFormSchemaRegistry */ has(name: string): boolean; + + getDocManager(): IDocumentManager; } export interface IJGISExternalCommand { diff --git a/packages/schema/src/schema/imageSource.json b/packages/schema/src/schema/imageSource.json index 22aab46a3..6d7163685 100644 --- a/packages/schema/src/schema/imageSource.json +++ b/packages/schema/src/schema/imageSource.json @@ -2,13 +2,13 @@ "type": "object", "description": "ImageSource", "title": "IImageSource", - "required": ["url", "coordinates"], + "required": ["path", "coordinates"], "additionalProperties": false, "properties": { - "url": { + "path": { "type": "string", "readOnly": true, - "description": "URL that points to an image" + "description": "Path that points to an image" }, "coordinates": { "type": "array", diff --git a/python/jupytergis_core/src/plugin.ts b/python/jupytergis_core/src/plugin.ts index a60b06ecc..22a9ad874 100644 --- a/python/jupytergis_core/src/plugin.ts +++ b/python/jupytergis_core/src/plugin.ts @@ -18,6 +18,7 @@ import { import { WidgetTracker } from '@jupyterlab/apputils'; import { IMainMenu } from '@jupyterlab/mainmenu'; import { ITranslator } from '@jupyterlab/translation'; +import { IDocumentManager } from '@jupyterlab/docmanager'; import { JupyterGISExternalCommandRegistry } from './externalcommand'; import { JupyterGISLayerBrowserRegistry } from './layerBrowserRegistry'; @@ -48,10 +49,13 @@ export const formSchemaRegistryPlugin: JupyterFrontEndPlugin { - const registry = new JupyterGISFormSchemaRegistry(); + activate: ( + app: JupyterFrontEnd, + docmanager: IDocumentManager + ): IJGISFormSchemaRegistry => { + const registry = new JupyterGISFormSchemaRegistry(docmanager); return registry; } }; diff --git a/python/jupytergis_core/src/schemaregistry.ts b/python/jupytergis_core/src/schemaregistry.ts index 77fe97528..ef314b0a3 100644 --- a/python/jupytergis_core/src/schemaregistry.ts +++ b/python/jupytergis_core/src/schemaregistry.ts @@ -1,9 +1,13 @@ import { IDict, IJGISFormSchemaRegistry } from '@jupytergis/schema'; import formSchema from '@jupytergis/schema/lib/_interface/forms.json'; +import { IDocumentManager } from '@jupyterlab/docmanager'; export class JupyterGISFormSchemaRegistry implements IJGISFormSchemaRegistry { - constructor() { + private _docManager: IDocumentManager; + + constructor(docManager: IDocumentManager) { this._registry = new Map(Object.entries(formSchema)); + this._docManager = docManager; } registerSchema(name: string, schema: IDict): void { @@ -22,5 +26,9 @@ export class JupyterGISFormSchemaRegistry implements IJGISFormSchemaRegistry { return this._registry; } + getDocManager(): IDocumentManager { + return this._docManager; + } + private _registry: Map; } diff --git a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py index 4cf9f1dbb..fb5c5d0b5 100644 --- a/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py +++ b/python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py @@ -316,7 +316,7 @@ def add_image_layer( source = { "type": SourceType.ImageSource, "name": f"{name} Source", - "parameters": {"url": url, "coordinates": coordinates}, + "parameters": {"path": url, "coordinates": coordinates}, } source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))