Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/base/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
7 changes: 7 additions & 0 deletions packages/base/src/dialogs/formdialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface ICreationFormWrapperProps extends ICreationFormProps {
* some extra errors or not.
*/
formErrorSignalPromise?: PromiseDelegate<Signal<Dialog<any>, 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 {
Expand Down Expand Up @@ -53,6 +58,7 @@ export const CreationFormWrapper = (props: ICreationFormWrapperProps) => {
ok={okSignal.current}
cancel={props.cancel}
formErrorSignal={formErrorSignal.current}
dialogOptions={props.dialogOptions}
/>
)
);
Expand Down Expand Up @@ -88,6 +94,7 @@ export class CreationFormDialog extends Dialog<IDict> {
okSignalPromise={okSignalPromise}
cancel={cancelCallback}
formErrorSignalPromise={formErrorSignalPromise}
dialogOptions={options}
/>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions packages/base/src/formbuilder/creationform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export interface ICreationFormProps {
* extra errors or not.
*/
formErrorSignal?: Signal<Dialog<any>, boolean>;

/**
* Configuration options for the dialog, including settings for layer data, source data,
* and other form-related parameters.
*/
dialogOptions?: any;
}

/**
Expand Down Expand Up @@ -206,6 +212,7 @@ export class CreationForm extends React.Component<ICreationFormProps, any> {
cancel={this.props.cancel}
formChangedSignal={this.sourceFormChangedSignal}
formErrorSignal={this.props.formErrorSignal}
dialogOptions={this.props.dialogOptions}
/>
</div>
)}
Expand All @@ -226,6 +233,7 @@ export class CreationForm extends React.Component<ICreationFormProps, any> {
cancel={this.props.cancel}
sourceFormChangedSignal={this.sourceFormChangedSignal}
formErrorSignal={this.props.formErrorSignal}
dialogOptions={this.props.dialogOptions}
/>
</div>
)}
Expand Down
4 changes: 4 additions & 0 deletions packages/base/src/formbuilder/editform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -118,10 +119,13 @@ export class EditForm extends React.Component<IEditFormProps, any> {
syncData={(properties: { [key: string]: any }) => {
this.syncObjectProperties(this.props.source, properties);
}}
formChangedSignal={this.sourceFormChangedSignal}
/>
</div>
)}
</div>
);
}
private sourceFormChangedSignal: Signal<React.Component<any>, IDict<any>> =
new Signal(this);
}
21 changes: 21 additions & 0 deletions packages/base/src/formbuilder/objectform/baseform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,12 @@ export interface IBaseFormProps {
* extra errors or not.
*/
formErrorSignal?: Signal<Dialog<any>, 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 => {
Expand Down Expand Up @@ -195,6 +202,20 @@ export class BaseForm extends React.Component<IBaseFormProps, IBaseFormStates> {
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
}
};
}
});
}

Expand Down
123 changes: 123 additions & 0 deletions packages/base/src/formbuilder/objectform/fileselectorwidget.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
const url = event.target.value;
setServerFilePath('');
setUrlPath(url);
props.onChange(url);
};

return (
<div>
<div>
<input
type="text"
className="jp-mod-styled"
value={serverFilePath || ''}
readOnly
style={{ width: '70%', marginRight: '10px' }}
/>
<button className="jp-mod-styled" onClick={handleBrowseServerFiles}>
Browse Server Files
</button>
</div>
<div>
<h3 className="jp-FormGroup-fieldLabel jp-FormGroup-contentItem">
Or enter external URL
</h3>
<input
type="text"
id="root_path"
className="jp-mod-styled"
onChange={handleURLChange}
value={urlPath || ''}
style={{ width: '100%' }}
/>
</div>
</div>
);
};
10 changes: 9 additions & 1 deletion packages/base/src/formbuilder/objectform/geojsonsource.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<any>) {
if (this.state.extraErrors?.path?.__errors?.length >= 1) {
showErrorMessage(
Expand Down
8 changes: 8 additions & 0 deletions packages/base/src/formbuilder/objectform/layerform.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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 };
}
}
}
2 changes: 1 addition & 1 deletion packages/base/src/mainview/mainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ export class MainView extends React.Component<IProps, IStates> {
const extent = [minX, minY, maxX, maxY];

const imageUrl = await loadFile({
filepath: sourceParameters.url,
filepath: sourceParameters.path,
type: 'ImageSource',
model: this._model
});
Expand Down
3 changes: 3 additions & 0 deletions packages/schema/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -246,6 +247,8 @@ export interface IJGISFormSchemaRegistry {
* @memberof IJGISFormSchemaRegistry
*/
has(name: string): boolean;

getDocManager(): IDocumentManager;
}

export interface IJGISExternalCommand {
Expand Down
6 changes: 3 additions & 3 deletions packages/schema/src/schema/imageSource.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions python/jupytergis_core/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,10 +49,13 @@ export const formSchemaRegistryPlugin: JupyterFrontEndPlugin<IJGISFormSchemaRegi
{
id: 'jupytergis:core:form-schema-registry',
autoStart: true,
requires: [],
requires: [IDocumentManager],
provides: IJGISFormSchemaRegistryToken,
activate: (app: JupyterFrontEnd): IJGISFormSchemaRegistry => {
const registry = new JupyterGISFormSchemaRegistry();
activate: (
app: JupyterFrontEnd,
docmanager: IDocumentManager
): IJGISFormSchemaRegistry => {
const registry = new JupyterGISFormSchemaRegistry(docmanager);
return registry;
}
};
Expand Down
10 changes: 9 additions & 1 deletion python/jupytergis_core/src/schemaregistry.ts
Original file line number Diff line number Diff line change
@@ -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<string, IDict>(Object.entries(formSchema));
this._docManager = docManager;
}

registerSchema(name: string, schema: IDict): void {
Expand All @@ -22,5 +26,9 @@ export class JupyterGISFormSchemaRegistry implements IJGISFormSchemaRegistry {
return this._registry;
}

getDocManager(): IDocumentManager {
return this._docManager;
}

private _registry: Map<string, IDict>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading