Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom editors #769

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
13 changes: 13 additions & 0 deletions services/madoc-ts/src/extensions/projects/editors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Project editors

At the moment there is only one project editor: Capture Model UI.

This will have a number of different editors for different types of capture models.

Elements of an editor:
- Editor
- Preview
- Review
- Metadata


Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const defaultEditor = {
metadata: {
label: 'Capture model editor',
},
// UI Elements.
// ReadOnlyViewer
// Editor
};
24 changes: 24 additions & 0 deletions services/madoc-ts/src/extensions/projects/editors/null-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { EditorRenderingConfig } from '../../../frontend/shared/capture-models/new/components/EditorSlots';

export const nullEditor = {
metadata: {
label: 'Null editor',
},

editor: {
sidebar: {
enabled: true,
components: {
TopLevelEditor: () => <div>Top level</div>,
SubmitButton: () => <div />,
} as Partial<EditorRenderingConfig>,
},
viewer: {
enabled: true,
component: () => <div>Viewer</div>,
controls: () => <div>controls</div>,
},
},
// UI Elements.
};
5 changes: 5 additions & 0 deletions services/madoc-ts/src/extensions/projects/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export type JsonProjectTemplate = {
defaults?: Partial<ProjectConfiguration>;
immutable?: Array<keyof ProjectConfiguration>;
frozen?: boolean;
// Editor shown on the frontend to the user.
editor?: {
type: string;
options?: any;
};
captureModels?: ModelEditorConfig;
tasks?: {
generateOnCreate?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export type RevisionsModel = {
// Field instances (for allowMultiple=true)
createNewFieldInstance: Action<
RevisionsModel,
{ path: Array<[string, string]>; revisionId?: string; property: string }
{ path: Array<[string, string]>; revisionId?: string; property: string; withId?: string }
>;
createNewEntityInstance: Action<
RevisionsModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,15 +612,15 @@ export const revisionStore: RevisionsModel = {
}
}),

createNewFieldInstance: action((state, { property, path, revisionId }) => {
createNewFieldInstance: action((state, { property, path, revisionId, withId }) => {
// Grab the parent entity where we want to add a new field.
const entity = getRevisionFieldFromPath<CaptureModel['document']>(state, path, revisionId);
if (!entity) {
throw new Error('invalid entity');
}

// Fork a new field from what already exists.
const newField = createNewFieldInstance(debug(entity), property);
const newField = createNewFieldInstance(debug(entity), property, false, null, withId);

if (newField) {
newField.revision = state.currentRevisionId || undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function createNewFieldInstance(
entity: CaptureModel['document'],
property: string,
multipleOverride = false,
revisionId?: string | null
revisionId?: string | null,
withId?: string
): BaseField {
// Grab the property itself from the entity.
const prop = entity.properties[property];
Expand All @@ -30,7 +31,7 @@ export function createNewFieldInstance(
}

// Modify the new field with defaults form the plugin store
newField.id = generateId();
newField.id = withId || generateId();
newField.value = copy(description.defaultValue);
if (newField.selector) {
newField.selector.id = generateId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { Store } from 'easy-peasy';
import { useMemo } from 'react';
import { Revisions, RevisionsModel } from '../editor/stores/revisions/index';
import { generateId } from '../helpers/generate-id';
import { getRevisionFieldFromPath } from '../helpers/get-revision-field-from-path';
import { useSelector } from '../plugin-api/hooks/use-selector';
import { CaptureModel } from '../types/capture-model';
import { BaseField } from '../types/field-types';

interface CaptureModelFnInstance<T> {
get(): T;
set(value: T): void;
update(fn: (value: T) => T): void;
}

interface CaptureModelFnList<T> extends CaptureModelFnInstance<T[]> {
push(value: T): void;
remove(value: T): void;
removeAt(index: number): void;
at(index: number): CaptureModelFnInstance<T>;
// Future?
// insertAt(index: number, value: T): void;
// move(from: number, to: number): void;
}

type UpdateFieldValueContext = RevisionsModel['updateFieldValue']['payload'];

class BaseFieldWrapper<T> implements CaptureModelFnInstance<T> {
store: Store<RevisionsModel>;
path: [string, string][];
constructor(store: Store<RevisionsModel>, path: [string, string][]) {
this.store = store;
this.path = path;
}

get(): T {
const field = getRevisionFieldFromPath<BaseField>(this.store.getState(), this.path);
return field ? field.value : undefined;
}

set(value: T) {
this.store.getActions().updateFieldValue({
path: this.path,
value,
});
}

update(fn: (value: T) => any) {
this.store.getActions().updateFieldValue({
path: this.path,
value: fn(this.get()),
});
}
}

class JsonFieldWrapper<T> implements CaptureModelFnInstance<T | undefined> {
store: Store<RevisionsModel>;
path: [string, string][];
constructor(store: Store<RevisionsModel>, path: [string, string][]) {
this.store = store;
this.path = path;
}

get() {
const field = getRevisionFieldFromPath<BaseField>(this.store.getState(), this.path);
try {
return field ? JSON.parse(field.value) : undefined;
} catch (err) {
return undefined;
}
}

set(value: T | undefined) {
this.store.getActions().updateFieldValue({
path: this.path,
value: value ? JSON.stringify(value) : value,
});
}

update(fn: (value: T | undefined) => any) {
this.store.getActions().updateFieldValue({
path: this.path,
value: fn(this.get()),
});
}
}

// Wraps a structure like:
// { 'property-name': [BaseFieldWrapper, BaseFieldWrapper, BaseFieldWrapper] }
class FieldListWrapper<T> implements CaptureModelFnList<T> {
path: [string, string][];
property: string;
store: Store<RevisionsModel>;
fields: BaseFieldWrapper<T>[] = [];
constructor(store: Store<RevisionsModel>, path: [string, string][], property: string) {
this.store = store;
this.property = property;
this.path = path;
const entity = getRevisionFieldFromPath<CaptureModel['document']>(store.getState(), path);
if (!entity) {
throw new Error('Could not find entity');
}

const propertyList = entity.properties[property];
for (const field of propertyList) {
if (field.type === 'entity') {
// TODO
} else {
this.fields.push(new BaseFieldWrapper(store, [...path, [property, field.id]]));
}
}
}

get(): T[] {
return this.fields.map(f => f.get());
}

at(index: number): CaptureModelFnInstance<T> {
return this.fields[index];
}

push(value: T) {
const actions = this.store.getActions();
const id = generateId();
actions.createNewFieldInstance({
path: this.path,
property: this.property,
withId: id,
});

actions.updateFieldValue({
path: [...this.path, [this.property, id]],
value,
});
}

remove(value: T) {
const index = this.fields.findIndex(f => f.get() === value);
if (index !== -1) {
this.removeAt(index);
}
}

removeAt(index: number) {
const actions = this.store.getActions();
const field = this.fields[index];
actions.removeInstance({
path: field.path,
});
}

set(): never {
throw new Error('Not implemented');
}

update(fn: (value: T[]) => any): never {
throw new Error('Not implemented');
}
}

export class BaseEntityWrapper implements CaptureModelFnInstance<CaptureModel['document']> {
store: Store<RevisionsModel>;
path: [string, string][];
properties: Map<string, FieldListWrapper<any>> = new Map();

constructor(store: Store<RevisionsModel>, path: [string, string][]) {
this.store = store;
this.path = path;

const entity = getRevisionFieldFromPath<CaptureModel['document']>(store.getState(), path);
if (!entity) {
throw new Error('Could not find entity');
}

for (const property of Object.keys(entity.properties)) {
this.properties.set(property, new FieldListWrapper(store, path, property));
}
}

getProperty<T>(property: string): CaptureModelFnList<T> {
return this.properties.get(property) as any;
}

get(): CaptureModel['document'] {
const entity = getRevisionFieldFromPath<CaptureModel['document']>(this.store.getState(), this.path);
if (!entity) {
throw new Error('Could not find entity');
}
return entity;
}

set(): never {
throw new Error('Cant overwrite entity');
}

update(fn: (value: CaptureModel['document']) => any): void {
throw new Error('Cant overwrite entity');
}
}

function useModelInstance() {
const currentRevisionId = Revisions.useStoreState(s => s.currentRevisionId);
const store = Revisions.useStore();

return useMemo(() => {
if (!store.getState().currentRevision) {
return null;
}

return new BaseEntityWrapper(store, []);
}, [currentRevisionId]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export const Annotorious = React.lazy(() => import('./Annotorious'));
71 changes: 71 additions & 0 deletions services/madoc-ts/src/frontend/shared/viewers/Annotorious.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useMemo, useRef } from 'react';
import * as Annotorious from '@recogito/annotorious-openseadragon';
import '@recogito/annotorious-openseadragon/dist/annotorious.min.css';
import { OpenSeadragonViewer } from '../../site/features/OpenSeadragonViewer.lazy';
import { Revisions } from '../capture-models/editor/stores/revisions/index';
import { BaseEntityWrapper } from '../capture-models/utility/capture-model-fn';

interface AnnotoriousOptions {
allowEmpty?: boolean;
disableEditor?: boolean;
disableSelect?: boolean;
drawOnSingleClick?: boolean;
formatters?: any[] | any;
fragmentUnit?: string;
gigapixelMode?: boolean;
handleRadius?: number;
hotkey?: any;
locale?: string;
messages?: any;
readOnly?: boolean;
widgets?: any[];
}

interface AnnotoriousInstance {
disableEditor: boolean;
disableSelect: boolean;
formatters: any[];
readOnly: boolean;
widgets: any[];
}

export default function AnnotoriousViewer() {
const osd = useRef<any>();
const annotorious = useRef<any>();

const revision = Revisions.useStoreState(s => s.currentRevision);
const currentRevisionId = Revisions.useStoreState(s => s.currentRevisionId);
const actions = Revisions.useStoreActions(a => a);

const store = Revisions.useStore();

const wrapper = useMemo(() => {
if (!store.getState().currentRevision) {
return null;
}

return new BaseEntityWrapper(store, []);
}, [currentRevisionId]);

console.log('wrapper', wrapper);

if (wrapper) {
// wrapper
// .getProperty('Date')
// .at(0)
// .set('Testing value');
// console.log(wrapper.getProperty('Date').get());
}

const setupAnnotorious = (viewer: any) => {
console.log('viewer.element', viewer.element);

setTimeout(() => {
annotorious.current = Annotorious(viewer, {
// config?
} as AnnotoriousOptions);
}, 1000);
};

return <OpenSeadragonViewer ref={osd} onReady={setupAnnotorious} />;
}
Loading