From 6d38f8fae991e0b70ad3d4c9c5f324548f93cdfa Mon Sep 17 00:00:00 2001 From: Meriem-BenIsmail Date: Fri, 10 Jan 2025 10:04:47 +0100 Subject: [PATCH 1/9] use filedialog of jupyterlab for path selection. switch between form and file dialog/ filepath modified relative path file name changed reopen the creation dialog if filedialog cancel button is clicked. --- packages/base/src/dialogs/formdialog.tsx | 7 ++ .../base/src/formbuilder/creationform.tsx | 8 ++ packages/base/src/formbuilder/editform.tsx | 4 + .../src/formbuilder/objectform/baseform.tsx | 21 ++++ .../objectform/fileselectorwidget.tsx | 99 +++++++++++++++++++ .../formbuilder/objectform/geojsonsource.ts | 10 +- .../src/formbuilder/objectform/layerform.ts | 9 ++ packages/schema/src/interfaces.ts | 3 + python/jupytergis_core/src/plugin.ts | 10 +- python/jupytergis_core/src/schemaregistry.ts | 10 +- 10 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 packages/base/src/formbuilder/objectform/fileselectorwidget.tsx 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..0ea986fa8 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..48484e0bc --- /dev/null +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -0,0 +1,99 @@ +import React 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 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 GeoJSON File', + manager: docManager + }); + + if (output.value && output.value.length > 0) { + const selectedFilePath = output.value[0].path; + + const relativePath = PathExt.relative( + formOptions.filePath, + selectedFilePath + ); + 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; + props.onChange(url); + }; + + return ( +
+
+ + +
+
+ + +
+
+ ); +}; 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..6db30d91c 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,12 @@ 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/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/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; } From 6e3185d1288f57342d9c20cbe883052c2944d9ba Mon Sep 17 00:00:00 2001 From: Meriem-BenIsmail Date: Wed, 15 Jan 2025 09:19:35 +0100 Subject: [PATCH 2/9] lint --- packages/base/src/formbuilder/editform.tsx | 2 +- .../base/src/formbuilder/objectform/fileselectorwidget.tsx | 2 +- packages/base/src/formbuilder/objectform/layerform.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/base/src/formbuilder/editform.tsx b/packages/base/src/formbuilder/editform.tsx index 0ea986fa8..6cc2c9947 100644 --- a/packages/base/src/formbuilder/editform.tsx +++ b/packages/base/src/formbuilder/editform.tsx @@ -127,5 +127,5 @@ export class EditForm extends React.Component { ); } private sourceFormChangedSignal: Signal, IDict> = - new Signal(this); + new Signal(this); } diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index 48484e0bc..08fd8ee38 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -7,7 +7,7 @@ import { PathExt } from '@jupyterlab/coreutils'; export const FileSelectorWidget = (props: any) => { const { options } = props; const { docManager, formOptions } = options; - + const handleBrowseServerFiles = async () => { try { const dialogElement = document.querySelector( diff --git a/packages/base/src/formbuilder/objectform/layerform.ts b/packages/base/src/formbuilder/objectform/layerform.ts index 6db30d91c..5b4acbf00 100644 --- a/packages/base/src/formbuilder/objectform/layerform.ts +++ b/packages/base/src/formbuilder/objectform/layerform.ts @@ -47,9 +47,8 @@ export class LayerPropertiesForm extends BaseForm { protected onFormChange(e: IChangeEvent): void { super.onFormChange(e); - if (this.props.dialogOptions){ + if (this.props.dialogOptions) { this.props.dialogOptions.layerData = { ...e.formData }; - } } } From 8ee39fb99fa8e18afb7c67a9a3e04a8207dd545f Mon Sep 17 00:00:00 2001 From: Meriem-BenIsmail Date: Wed, 15 Jan 2025 10:55:17 +0100 Subject: [PATCH 3/9] add id for path input --- .../base/src/formbuilder/objectform/fileselectorwidget.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index 08fd8ee38..70c155c8d 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -73,6 +73,7 @@ export const FileSelectorWidget = (props: any) => {
{
- +

Enter URL

Date: Wed, 15 Jan 2025 11:10:37 +0100 Subject: [PATCH 4/9] path id --- packages/base/src/formbuilder/objectform/fileselectorwidget.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index 70c155c8d..a3b9d8685 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -87,7 +87,6 @@ export const FileSelectorWidget = (props: any) => {

Enter URL

Date: Wed, 15 Jan 2025 11:11:27 +0100 Subject: [PATCH 5/9] lint --- .../base/src/formbuilder/objectform/fileselectorwidget.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index a3b9d8685..b54b6feea 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -84,7 +84,9 @@ export const FileSelectorWidget = (props: any) => {
-

Enter URL

+

+ Enter URL +

Date: Wed, 15 Jan 2025 11:28:38 +0100 Subject: [PATCH 6/9] file path id added to the right input --- packages/base/src/formbuilder/objectform/fileselectorwidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index b54b6feea..172dd1c89 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -73,7 +73,6 @@ export const FileSelectorWidget = (props: any) => {
{ Date: Wed, 15 Jan 2025 12:16:40 +0100 Subject: [PATCH 7/9] remove geojson form filedialog title to make it more general. --- packages/base/src/formbuilder/objectform/fileselectorwidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index 172dd1c89..a1a90b893 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -26,7 +26,7 @@ export const FileSelectorWidget = (props: any) => { } const output = await FileDialog.getOpenFiles({ - title: 'Select a GeoJSON File', + title: 'Select a File', manager: docManager }); From 34ae884ea962b2694e14219c33e9244ba6d6db37 Mon Sep 17 00:00:00 2001 From: Meriem-BenIsmail Date: Wed, 15 Jan 2025 16:18:28 +0100 Subject: [PATCH 8/9] changed image url to image path. --- packages/base/src/commands.ts | 2 +- packages/base/src/mainview/mainView.tsx | 2 +- packages/schema/src/schema/imageSource.json | 6 +++--- .../jupytergis_lab/jupytergis_lab/notebook/gis_document.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/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/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_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)) From 765a97200aceeffe52d43712bc334d9140dc5fc3 Mon Sep 17 00:00:00 2001 From: Meriem-BenIsmail Date: Thu, 16 Jan 2025 10:45:53 +0100 Subject: [PATCH 9/9] independant local/external file field values. --- .../objectform/fileselectorwidget.tsx | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx index a1a90b893..d152027ac 100644 --- a/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx +++ b/packages/base/src/formbuilder/objectform/fileselectorwidget.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { FileDialog } from '@jupyterlab/filebrowser'; import { Dialog } from '@jupyterlab/apputils'; import { CreationFormDialog } from '../../dialogs/formdialog'; @@ -8,6 +8,24 @@ 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( @@ -37,6 +55,9 @@ export const FileSelectorWidget = (props: any) => { formOptions.filePath, selectedFilePath ); + + setServerFilePath(relativePath); + setUrlPath(''); props.onChange(relativePath); if (dialogElement) { @@ -65,6 +86,8 @@ export const FileSelectorWidget = (props: any) => { const handleURLChange = (event: React.ChangeEvent) => { const url = event.target.value; + setServerFilePath(''); + setUrlPath(url); props.onChange(url); }; @@ -74,7 +97,7 @@ export const FileSelectorWidget = (props: any) => { @@ -84,14 +107,14 @@ export const FileSelectorWidget = (props: any) => {

- Enter URL + Or enter external URL