Skip to content

Commit

Permalink
#4040 - Ability to store preferences from client-side code
Browse files Browse the repository at this point in the history
* Added `ClientSideUserPreferencesProvider` interface that can be implemented by Spring components to permit access to certain preference keys including their validation
* Added LoadPreferences and SavePreferences calls to the JS API
* If an annotation has no label, show the layer name in the annotation browser sidebar instead of "No label"
* Removed unused DiamEditorBase class
  • Loading branch information
reckart committed May 30, 2023
1 parent ccc9cf6 commit a22e0d2
Show file tree
Hide file tree
Showing 45 changed files with 828 additions and 120 deletions.
4 changes: 4 additions & 0 deletions inception/inception-api-editor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
<groupId>de.tudarmstadt.ukp.inception.app</groupId>
<artifactId>inception-api-render</artifactId>
</dependency>
<dependency>
<groupId>de.tudarmstadt.ukp.inception.app</groupId>
<artifactId>inception-preferences</artifactId>
</dependency>
<dependency>
<groupId>de.tudarmstadt.ukp.inception.app</groupId>
<artifactId>inception-model</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@

import static java.lang.Integer.MIN_VALUE;

import java.io.IOException;
import java.util.Optional;

import org.apache.wicket.model.IModel;

import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;
import de.tudarmstadt.ukp.inception.support.xml.sanitizer.PolicyCollection;

public interface AnnotationEditorFactory
{
Expand Down Expand Up @@ -58,4 +62,6 @@ AnnotationEditorBase create(String id, IModel<AnnotatorState> aModel,
* the annotator state
*/
void initState(AnnotatorState aState);

Optional<PolicyCollection> getPolicy() throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
*/
package de.tudarmstadt.ukp.inception.editor;

import java.io.IOException;
import java.util.Optional;

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.Ordered;

import de.tudarmstadt.ukp.inception.support.xml.sanitizer.PolicyCollection;

public abstract class AnnotationEditorFactoryImplBase
implements BeanNameAware, Ordered, AnnotationEditorFactory
{
Expand All @@ -42,4 +47,10 @@ public int getOrder()
{
return Ordered.LOWEST_PRECEDENCE;
}

@Override
public Optional<PolicyCollection> getPolicy() throws IOException
{
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ public class BratAnnotationEditor
private DifferentialRenderingSupport diffRenderSupport;

public BratAnnotationEditor(String id, IModel<AnnotatorState> aModel,
final AnnotationActionHandler aActionHandler, final CasProvider aCasProvider)
final AnnotationActionHandler aActionHandler, final CasProvider aCasProvider,
String aEditorFactoryId)
{
super(id, aModel, aActionHandler, aCasProvider);
super(id, aModel, aActionHandler, aCasProvider, aEditorFactoryId);

add(visibleWhen(getModel().map(AnnotatorState::getProject).isPresent()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public String getDisplayName()
public AnnotationEditorBase create(String aId, IModel<AnnotatorState> aModel,
AnnotationActionHandler aActionHandler, CasProvider aCasProvider)
{
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider);
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider, getBeanName());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public int accepts(Project aProject, String aFormat)
public AnnotationEditorBase create(String aId, IModel<AnnotatorState> aModel,
AnnotationActionHandler aActionHandler, CasProvider aCasProvider)
{
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider);
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider, getBeanName());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public String getDisplayName()
public AnnotationEditorBase create(String aId, IModel<AnnotatorState> aModel,
AnnotationActionHandler aActionHandler, CasProvider aCasProvider)
{
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider);
return new BratAnnotationEditor(aId, aModel, aActionHandler, aCasProvider, getBeanName());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@ public class DiamAnnotationBrowser
private @SpringBean ServletContext servletContext;
private @SpringBean PreferencesService userPrefService;

private final String userPreferencesKey;

private DiamAjaxBehavior diamBehavior;

public DiamAnnotationBrowser(String aId)
public DiamAnnotationBrowser(String aId, String aUserPreferencesKey)
{
super(aId);
userPreferencesKey = aUserPreferencesKey;
}

@Override
Expand Down Expand Up @@ -87,7 +90,8 @@ protected void onConfigure()
"ajaxEndpointUrl", diamBehavior.getCallbackUrl(), //
"wsEndpointUrl", constructEndpointUrl(), //
"topicChannel", viewport.getTopic(), //
"pinnedGroups", managerPrefs.getPinnedGroups());
"pinnedGroups", managerPrefs.getPinnedGroups(), "userPreferencesKey",
userPreferencesKey);

// model will be added as props to Svelte component
setDefaultModel(Model.ofMap(properties));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@ public class DiamSidebar
{
private static final long serialVersionUID = -3062641971181309172L;

private final String userPreferencesKey;

private DiamAnnotationBrowser browser;

public DiamSidebar(String aId, IModel<AnnotatorState> aModel,
AnnotationActionHandler aActionHandler, CasProvider aCasProvider,
AnnotationPage aAnnotationPage)
AnnotationPage aAnnotationPage, String aUserPreferencesKey)
{
super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage);

add(browser = new DiamAnnotationBrowser("vis"));
userPreferencesKey = aUserPreferencesKey;
add(browser = new DiamAnnotationBrowser("vis", userPreferencesKey));
}

@OnEvent
public void onDocumentOpenedEvent(DocumentOpenedEvent aEvent)
{
browser = (DiamAnnotationBrowser) browser.replaceWith(new DiamAnnotationBrowser("vis"));
browser = (DiamAnnotationBrowser) browser
.replaceWith(new DiamAnnotationBrowser("vis", userPreferencesKey));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,48 @@
*/
package de.tudarmstadt.ukp.inception.diam.sidebar;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;

import org.apache.wicket.model.IModel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;

import com.networknt.schema.JsonSchema;

import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon;
import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType;
import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider;
import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil;
import de.tudarmstadt.ukp.clarin.webanno.support.WatchedResourceFile;
import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage;
import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase;
import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase;
import de.tudarmstadt.ukp.inception.diam.sidebar.config.AnnotationBrowserSidebarAutoConfiguration;
import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.inception.preferences.ClientSidePreferencesKey;
import de.tudarmstadt.ukp.inception.preferences.ClientSideUserPreferencesProvider;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;

@ConditionalOnExpression("${websocket.enabled:true}")
@Component
/**
* <p>
* This class is exposed as a Spring Component via
* {@link AnnotationBrowserSidebarAutoConfiguration#annotationBrowserSidebarFactory}.
* </p>
*/
public class DiamSidebarFactory
extends AnnotationSidebarFactory_ImplBase
implements ClientSideUserPreferencesProvider
{
private WatchedResourceFile<JsonSchema> userPreferencesSchema;

public DiamSidebarFactory()
{
var userPreferencesSchemaFile = getClass()
.getResource("DiamSidebarFactoryUserPreferences.schema.json");
userPreferencesSchema = new WatchedResourceFile<>(userPreferencesSchemaFile,
JSONUtil::loadJsonSchema);
}

@Override
public String getDisplayName()
{
Expand All @@ -58,6 +82,21 @@ public AnnotationSidebar_ImplBase create(String aId, IModel<AnnotatorState> aMod
AnnotationActionHandler aActionHandler, CasProvider aCasProvider,
AnnotationPage aAnnotationPage)
{
return new DiamSidebar(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage);
return new DiamSidebar(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage,
getUserPreferencesKey().get().getClientSideKey());
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Optional<ClientSidePreferencesKey<Map>> getUserPreferencesKey()
{
return Optional.of(
new ClientSidePreferencesKey<>(Map.class, "annotation/annotation-browser-sidebar"));
}

@Override
public Optional<JsonSchema> getUserPreferencesSchema() throws IOException
{
return userPreferencesSchema.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft/2020-12/schema#",
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["by-label", "by-position"]
}
},
"required": ["mode"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.tudarmstadt.ukp.inception.diam.sidebar.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import de.tudarmstadt.ukp.inception.diam.sidebar.DiamSidebarFactory;

@ConditionalOnWebApplication
@Configuration
public class AnnotationBrowserSidebarAutoConfiguration
{
@ConditionalOnExpression("${websocket.enabled:true}")
@Bean
public DiamSidebarFactory annotationBrowserSidebarFactory()
{
return new DiamSidebarFactory();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
de.tudarmstadt.ukp.inception.diam.sidebar.config.AnnotationBrowserSidebarAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { writable } from 'svelte/store'

export const groupingMode = writable('by-label')
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import { compareOffsets } from "@inception-project/inception-js-api/src/model/Offsets";
import LabelBadge from "./LabelBadge.svelte";
import SpanText from "./SpanText.svelte";
import { compareSpanText, groupBy, uniqueLabels } from "./Utils";
import { compareSpanText, groupBy, renderLabel, uniqueLabels } from "./Utils";
export let ajaxClient: DiamAjax;
export let data: AnnotatedText;
Expand All @@ -45,7 +45,7 @@
const spans = data?.spans.values() || []
groupedAnnotations = groupBy(
[...spans, ...relations],
(s) => s.label || ""
(s) => renderLabel(s)
)
for (const items of Object.values(groupedAnnotations)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,26 @@
import { get_current_component } from 'svelte/internal'
import { AnnotatedText, unpackCompactAnnotatedTextV2 } from "@inception-project/inception-js-api"
import { factory } from "@inception-project/inception-diam"
import { groupingMode } from "./AnnotationBrowserState"
import AnnotationsByPositionList from "./AnnotationsByPositionList.svelte"
import AnnotationsByLabelList from "./AnnotationsByLabelList.svelte"
export let wsEndpointUrl: string
export let topicChannel: string
export let ajaxEndpointUrl: string
export let pinnedGroups: string[]
export let userPreferencesKey: string
let connected = false
let element = null;
let self = get_current_component()
let mode = 'Group by label';
let modes = [
'Group by position',
'Group by label'
]
let defaultPreferences = { 'mode': 'by-label' }
let preferences = Object.assign({}, defaultPreferences)
let modes = {
'by-position': 'Group by position',
'by-label': 'Group by label'
}
let data: AnnotatedText
Expand All @@ -46,6 +49,17 @@
let ajaxClient = factory().createAjaxClient(ajaxEndpointUrl)
ajaxClient.loadPreferences(userPreferencesKey).then((p) => {
preferences = Object.assign(preferences, defaultPreferences, p)
console.log('Loaded preferences', preferences)
groupingMode.set(preferences.mode || defaultPreferences.mode)
groupingMode.subscribe(mode => {
preferences.mode = mode
ajaxClient.savePreferences(userPreferencesKey, preferences)
})
});
export function messageRecieved(d) {
if (!document.body.contains(element)) {
console.debug("Element is not part of the DOM anymore. Disconnecting and suiciding.")
Expand All @@ -67,16 +81,16 @@
connected = false
}
onMount(async () => connect())
onMount(async () => connect())
onDestroy(async () => disconnect())
</script>

<div class="flex-content flex-v-container" bind:this={element}>
<select bind:value={mode} class="form-select">
{#each modes as value}<option {value}>{value}</option>{/each}
<select bind:value={$groupingMode} class="form-select">
{#each Object.keys(modes) as value}<option {value}>{modes[value]}</option>{/each}
</select>
{#if mode=='Group by position'}
{#if $groupingMode=='by-position'}
<AnnotationsByPositionList {ajaxClient} {data} />
{:else}
<AnnotationsByLabelList {ajaxClient} {data} {pinnedGroups} />
Expand Down
Loading

0 comments on commit a22e0d2

Please sign in to comment.