Merge branch 'develop'

This commit is contained in:
Paulo Gustavo Veiga 2022-02-11 17:53:16 -08:00
commit e1f71612ba
56 changed files with 858 additions and 374 deletions

View File

@ -37,7 +37,7 @@ declare global {
} }
export type EditorPropsType = { export type EditorPropsType = {
initCallback?: () => void; initCallback?: (locale: string) => void;
mapId?: number; mapId?: number;
isTryMode: boolean; isTryMode: boolean;
readOnlyMode: boolean; readOnlyMode: boolean;
@ -60,7 +60,7 @@ const loadLocaleData = (locale: string) => {
} }
} }
const initMindplot = () => { const initMindplot = (locale: string) => {
// Change page title ... // Change page title ...
document.title = `${global.mapTitle} | WiseMapping `; document.title = `${global.mapTitle} | WiseMapping `;
@ -95,7 +95,7 @@ const initMindplot = () => {
(global.userOptions?.zoom != undefined (global.userOptions?.zoom != undefined
? Number.parseFloat(global.userOptions.zoom as string) ? Number.parseFloat(global.userOptions.zoom as string)
: 0.8), : 0.8),
locale: global.locale, locale: locale,
}); });
// Build designer ... // Build designer ...
@ -119,11 +119,11 @@ const Editor = ({
onAction, onAction,
}: EditorPropsType): React.ReactElement => { }: EditorPropsType): React.ReactElement => {
React.useEffect(() => { React.useEffect(() => {
initCallback(); initCallback(locale);
}, []); }, []);
return ( return (
<IntlProvider locale={locale} defaultLocale="en" messages={loadLocaleData(locale)}> <IntlProvider locale={locale} messages={loadLocaleData(locale)}>
<Toolbar <Toolbar
isTryMode={isTryMode} isTryMode={isTryMode}
onAction={onAction} onAction={onAction}

View File

@ -21,6 +21,7 @@ import { $assert } from '@wisemapping/core-js';
import Point from '@wisemapping/web2d'; import Point from '@wisemapping/web2d';
import { Mindmap } from '..'; import { Mindmap } from '..';
import CommandContext from './CommandContext'; import CommandContext from './CommandContext';
import ControlPoint from './ControlPoint';
import Events from './Events'; import Events from './Events';
import NodeModel from './model/NodeModel'; import NodeModel from './model/NodeModel';
import RelationshipModel from './model/RelationshipModel'; import RelationshipModel from './model/RelationshipModel';
@ -44,7 +45,7 @@ abstract class ActionDispatcher extends Events {
abstract moveTopic(topicId: number, position: Point): void; abstract moveTopic(topicId: number, position: Point): void;
abstract moveControlPoint(ctrlPoint: this, point: Point): void; abstract moveControlPoint(ctrlPoint: ControlPoint, point: Point): void;
abstract changeFontFamilyToTopic(topicIds: number[], fontFamily: string): void; abstract changeFontFamilyToTopic(topicIds: number[], fontFamily: string): void;

View File

@ -35,7 +35,6 @@ class CentralTopic extends Topic {
}); });
} }
workoutIncomingConnectionPoint(): Point { workoutIncomingConnectionPoint(): Point {
return this.getPosition(); return this.getPosition();
} }

View File

@ -23,8 +23,11 @@ abstract class Command {
static _uuid: number; static _uuid: number;
private _discardDuplicated: string;
constructor() { constructor() {
this._id = Command._nextUUID(); this._id = Command._nextUUID();
this._discardDuplicated = undefined;
} }
abstract execute(commandContext: CommandContext): void; abstract execute(commandContext: CommandContext): void;
@ -42,6 +45,14 @@ abstract class Command {
this._uuid += 1; this._uuid += 1;
return this._uuid; return this._uuid;
} }
get discardDuplicated(): string {
return this._discardDuplicated;
}
set discardDuplicated(value: string) {
this._discardDuplicated = value;
}
} }
export default Command; export default Command;

View File

@ -20,8 +20,33 @@ import { $defined } from '@wisemapping/core-js';
import Shape from './util/Shape'; import Shape from './util/Shape';
import ActionDispatcher from './ActionDispatcher'; import ActionDispatcher from './ActionDispatcher';
import Workspace from './Workspace';
class ControlPoint { class ControlPoint {
private control1: Elipse;
private control2: Elipse;
private _controlPointsController: Elipse[];
private _controlLines: Line[];
private _isBinded: boolean;
_line: Line;
private _workspace: Workspace;
private _endPoint: any[];
private _orignalCtrlPoint: any;
private _controls: any;
private _mouseMoveFunction: (e: Event) => void;
private _mouseUpFunction: (e: Event) => void;
constructor() { constructor() {
this.control1 = new Elipse({ this.control1 = new Elipse({
width: 6, width: 6,
@ -70,7 +95,7 @@ class ControlPoint {
}); });
} }
setLine(line) { setLine(line: Line) {
if ($defined(this._line)) { if ($defined(this._line)) {
this._removeLine(); this._removeLine();
} }
@ -93,7 +118,7 @@ class ControlPoint {
if ($defined(this._line)) this._createControlPoint(); if ($defined(this._line)) this._createControlPoint();
} }
_createControlPoint() { private _createControlPoint() {
this._controls = this._line.getLine().getControlPoints(); this._controls = this._line.getLine().getControlPoints();
let pos = this._line.getLine().getFrom(); let pos = this._line.getLine().getFrom();
this._controlPointsController[0].setPosition( this._controlPointsController[0].setPosition(
@ -117,11 +142,11 @@ class ControlPoint {
); );
} }
_removeLine() { private _removeLine() {
// Overwrite default behaviour ... // Overwrite default behaviour ...
} }
_mouseDown(event, point, me) { private _mouseDown(event: Event, point, me) {
if (!this._isBinded) { if (!this._isBinded) {
this._isBinded = true; this._isBinded = true;
this._mouseMoveFunction = (e) => { this._mouseMoveFunction = (e) => {
@ -129,7 +154,7 @@ class ControlPoint {
}; };
this._workspace.getScreenManager().addEvent('mousemove', this._mouseMoveFunction); this._workspace.getScreenManager().addEvent('mousemove', this._mouseMoveFunction);
this._mouseUpFunction = (e) => { this._mouseUpFunction = (e: Event) => {
me._mouseUp(e, point, me); me._mouseUp(e, point, me);
}; };
this._workspace.getScreenManager().addEvent('mouseup', this._mouseUpFunction); this._workspace.getScreenManager().addEvent('mouseup', this._mouseUpFunction);
@ -139,7 +164,7 @@ class ControlPoint {
return false; return false;
} }
_mouseMoveEvent(event, point) { private _mouseMoveEvent(event: MouseEvent, point: Point) {
const screen = this._workspace.getScreenManager(); const screen = this._workspace.getScreenManager();
const pos = screen.getWorkspaceMousePosition(event); const pos = screen.getWorkspaceMousePosition(event);
@ -162,7 +187,7 @@ class ControlPoint {
this._line.getLine().updateLine(point); this._line.getLine().updateLine(point);
} }
_mouseUp(event, point) { private _mouseUp(event: MouseEvent, point: Point) {
this._workspace.getScreenManager().removeEvent('mousemove', this._mouseMoveFunction); this._workspace.getScreenManager().removeEvent('mousemove', this._mouseMoveFunction);
this._workspace.getScreenManager().removeEvent('mouseup', this._mouseUpFunction); this._workspace.getScreenManager().removeEvent('mouseup', this._mouseUpFunction);
@ -171,13 +196,13 @@ class ControlPoint {
this._isBinded = false; this._isBinded = false;
} }
_mouseClick(event) { _mouseClick(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
setVisibility(visible) { setVisibility(visible: boolean) {
if (visible) { if (visible) {
this._controlLines[0].moveToFront(); this._controlLines[0].moveToFront();
this._controlLines[1].moveToFront(); this._controlLines[1].moveToFront();
@ -190,7 +215,7 @@ class ControlPoint {
this._controlLines[1].setVisibility(visible); this._controlLines[1].setVisibility(visible);
} }
addToWorkspace(workspace) { addToWorkspace(workspace: Workspace): void {
this._workspace = workspace; this._workspace = workspace;
workspace.append(this._controlPointsController[0]); workspace.append(this._controlPointsController[0]);
workspace.append(this._controlPointsController[1]); workspace.append(this._controlPointsController[1]);
@ -198,7 +223,7 @@ class ControlPoint {
workspace.append(this._controlLines[1]); workspace.append(this._controlLines[1]);
} }
removeFromWorkspace(workspace) { removeFromWorkspace(workspace: Workspace) {
this._workspace = null; this._workspace = null;
workspace.removeChild(this._controlPointsController[0]); workspace.removeChild(this._controlPointsController[0]);
workspace.removeChild(this._controlPointsController[1]); workspace.removeChild(this._controlPointsController[1]);
@ -206,20 +231,21 @@ class ControlPoint {
workspace.removeChild(this._controlLines[1]); workspace.removeChild(this._controlLines[1]);
} }
getControlPoint(index) { getControlPoint(index: number): ControlPoint {
return this._controls[index]; return this._controls[index];
} }
getOriginalEndPoint(index) { getOriginalEndPoint(index: number) {
return this._endPoint[index]; return this._endPoint[index];
} }
getOriginalCtrlPoint(index) { getOriginalCtrlPoint(index: number): ControlPoint {
return this._orignalCtrlPoint[index]; return this._orignalCtrlPoint[index];
} }
static FROM = 0;
static TO = 1;
} }
ControlPoint.FROM = 0;
ControlPoint.TO = 1;
export default ControlPoint; export default ControlPoint;

View File

@ -137,8 +137,8 @@ class Designer extends Events {
} }
private _registerWheelEvents(): void { private _registerWheelEvents(): void {
const zoomFactor = 1.006; const zoomFactor = 1.02;
document.addEventListener('wheel', (event) => { document.addEventListener('wheel', (event: WheelEvent) => {
if (event.deltaX > 0 || event.deltaY > 0) { if (event.deltaX > 0 || event.deltaY > 0) {
this.zoomOut(zoomFactor); this.zoomOut(zoomFactor);
} else { } else {

View File

@ -22,7 +22,8 @@ import { Designer } from '..';
import Topic from './Topic'; import Topic from './Topic';
class DesignerKeyboard extends Keyboard { class DesignerKeyboard extends Keyboard {
static _instance: any; // eslint-disable-next-line no-use-before-define
static _instance: DesignerKeyboard;
constructor(designer: Designer) { constructor(designer: Designer) {
super(); super();
@ -79,14 +80,14 @@ class DesignerKeyboard extends Keyboard {
this.addShortcut( this.addShortcut(
['tab'], (eventevent: Event) => { ['tab'], (eventevent: Event) => {
designer.createChildForSelectedNode(); designer.createChildForSelectedNode();
event.preventDefault(); eventevent.preventDefault();
event.stopPropagation(); eventevent.stopPropagation();
}, },
); );
this.addShortcut( this.addShortcut(
['meta+enter'], (eventevent: Event) => { ['meta+enter'], (eventevent: Event) => {
event.preventDefault(); eventevent.preventDefault();
event.stopPropagation(); eventevent.stopPropagation();
designer.createChildForSelectedNode(); designer.createChildForSelectedNode();
}, },
); );
@ -244,7 +245,7 @@ class DesignerKeyboard extends Keyboard {
const excludes = ['esc', 'escape', 'f1', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12']; const excludes = ['esc', 'escape', 'f1', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'];
$(document).on('keypress', (event) => { $(document).on('keypress', (event) => {
let keyCode; let keyCode: number;
// Firefox doesn't skip special keys for keypress event... // Firefox doesn't skip special keys for keypress event...
if (event.key && excludes.includes(event.key.toLowerCase())) { if (event.key && excludes.includes(event.key.toLowerCase())) {
return; return;
@ -256,6 +257,7 @@ class DesignerKeyboard extends Keyboard {
keyCode = event.keyCode; keyCode = event.keyCode;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jq: any = $; const jq: any = $;
const specialKey = jq.hotkeys.specialKeys[keyCode]; const specialKey = jq.hotkeys.specialKeys[keyCode];
if (['enter', 'capslock'].indexOf(specialKey) === -1 && !jq.hotkeys.shiftNums[keyCode]) { if (['enter', 'capslock'].indexOf(specialKey) === -1 && !jq.hotkeys.shiftNums[keyCode]) {

View File

@ -16,15 +16,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { $assert } from '@wisemapping/core-js'; import { $assert } from '@wisemapping/core-js';
import Command from './Command';
import CommandContext from './CommandContext';
class DesignerUndoManager { class DesignerUndoManager {
private _undoQueue: Command[];
private _redoQueue: Command[];
private _baseId: number;
constructor() { constructor() {
this._undoQueue = []; this._undoQueue = [];
this._redoQueue = []; this._redoQueue = [];
this._baseId = 0; this._baseId = 0;
} }
enqueue(command) { enqueue(command: Command) {
$assert(command, 'Command can not be null'); $assert(command, 'Command can not be null');
const { length } = this._undoQueue; const { length } = this._undoQueue;
if (command.discardDuplicated && length > 0) { if (command.discardDuplicated && length > 0) {
@ -39,7 +47,7 @@ class DesignerUndoManager {
this._redoQueue = []; this._redoQueue = [];
} }
execUndo(commandContext) { execUndo(commandContext: CommandContext) {
if (this._undoQueue.length > 0) { if (this._undoQueue.length > 0) {
const command = this._undoQueue.pop(); const command = this._undoQueue.pop();
this._redoQueue.push(command); this._redoQueue.push(command);

View File

@ -17,9 +17,25 @@
*/ */
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import DragTopic from './DragTopic'; import DragTopic from './DragTopic';
import EventBusDispatcher from './layout/EventBusDispatcher';
import Workspace from './Workspace';
class DragManager { class DragManager {
constructor(workspace, eventDispatcher) { private _workspace: Workspace;
private _designerModel: Workspace;
private _isDragInProcess: boolean;
private _eventDispatcher: EventBusDispatcher;
private _listeners;
private _mouseMoveListener;
private _mouseUpListener;
constructor(workspace: Workspace, eventDispatcher: EventBusDispatcher) {
this._workspace = workspace; this._workspace = workspace;
this._designerModel = workspace; this._designerModel = workspace;
this._listeners = {}; this._listeners = {};
@ -34,7 +50,7 @@ class DragManager {
const screen = workspace.getScreenManager(); const screen = workspace.getScreenManager();
const dragManager = this; const dragManager = this;
const me = this; const me = this;
const mouseDownListener = function mouseDownListener(event) { const mouseDownListener = function mouseDownListener() {
if (workspace.isWorkspaceEventsEnabled()) { if (workspace.isWorkspaceEventsEnabled()) {
// Disable double drag... // Disable double drag...
workspace.enableWorkspaceEvents(false); workspace.enableWorkspaceEvents(false);
@ -62,11 +78,11 @@ class DragManager {
node.addEvent('mousedown', mouseDownListener); node.addEvent('mousedown', mouseDownListener);
} }
remove(node) { remove() {
throw new Error('Not implemented: DragManager.prototype.remove'); throw new Error('Not implemented: DragManager.prototype.remove');
} }
_buildMouseMoveListener(workspace, dragNode, dragManager) { protected _buildMouseMoveListener(workspace: Workspace, dragNode, dragManager: DragManager) {
const screen = workspace.getScreenManager(); const screen = workspace.getScreenManager();
const me = this; const me = this;
const result = (event) => { const result = (event) => {
@ -98,7 +114,7 @@ class DragManager {
return result; return result;
} }
_buildMouseUpListener(workspace, node, dragNode, dragManager) { protected _buildMouseUpListener(workspace: Workspace, node, dragNode, dragManager: DragManager) {
const screen = workspace.getScreenManager(); const screen = workspace.getScreenManager();
const me = this; const me = this;
const result = (event) => { const result = (event) => {

View File

@ -27,6 +27,7 @@ import SizeType from './SizeType';
class MainTopic extends Topic { class MainTopic extends Topic {
private INNER_RECT_ATTRIBUTES: { stroke: string; }; private INNER_RECT_ATTRIBUTES: { stroke: string; };
/** /**
* @extends mindplot.Topic * @extends mindplot.Topic
* @constructs * @constructs
@ -74,7 +75,6 @@ class MainTopic extends Topic {
return group; return group;
} }
updateTopicShape(targetTopic: Topic) { updateTopicShape(targetTopic: Topic) {
// Change figure based on the connected topic ... // Change figure based on the connected topic ...
const model = this.getModel(); const model = this.getModel();

View File

@ -258,7 +258,7 @@ class Relationship extends ConnectionLine {
} }
// @typescript-eslint/ban-types // @typescript-eslint/ban-types
addEvent(eventType: string, listener: any) { addEvent(eventType: string, listener) {
let type = eventType; let type = eventType;
// Translate to web 2d events ... // Translate to web 2d events ...
if (type === 'onfocus') { if (type === 'onfocus') {

View File

@ -28,7 +28,7 @@ class RelationshipPivot {
private _designer: Designer; private _designer: Designer;
private _mouseMoveEvent: MouseEvent; private _mouseMoveEvent;
private _onClickEvent: (event: MouseEvent) => void; private _onClickEvent: (event: MouseEvent) => void;

View File

@ -17,6 +17,7 @@
*/ */
import { $assert } from '@wisemapping/core-js'; import { $assert } from '@wisemapping/core-js';
import { Point } from '@wisemapping/web2d'; import { Point } from '@wisemapping/web2d';
import Icon from './Icon';
import Topic from './Topic'; import Topic from './Topic';
class ScreenManager { class ScreenManager {
@ -75,7 +76,7 @@ class ScreenManager {
fireEvent(type: string, event: UIEvent = null) { fireEvent(type: string, event: UIEvent = null) {
if (type === 'click') { if (type === 'click') {
this._clickEvents.forEach((listener: (arg0: any, arg1: any) => void) => { this._clickEvents.forEach((listener) => {
listener(type, event); listener(type, event);
}); });
} else { } else {
@ -101,7 +102,7 @@ class ScreenManager {
return { x, y }; return { x, y };
} }
getWorkspaceIconPosition(e: { getImage: () => any; getSize: () => any; getGroup: () => any; }) { getWorkspaceIconPosition(e: Icon) {
// Retrieve current icon position. // Retrieve current icon position.
const image = e.getImage(); const image = e.getImage();
const elementPosition = image.getPosition(); const elementPosition = image.getPosition();

View File

@ -20,11 +20,12 @@ import { Elipse } from '@wisemapping/web2d';
import TopicConfig from './TopicConfig'; import TopicConfig from './TopicConfig';
import ActionDispatcher from './ActionDispatcher'; import ActionDispatcher from './ActionDispatcher';
import Topic from './Topic'; import Topic from './Topic';
import IconGroup from './IconGroup';
class ShirinkConnector { class ShirinkConnector {
private _isShrink: boolean; private _isShrink: boolean;
private _ellipse: any;
private _ellipse: Elipse;
constructor(topic: Topic) { constructor(topic: Topic) {
this._isShrink = false; this._isShrink = false;
const ellipse = new Elipse(TopicConfig.INNER_RECT_ATTRIBUTES); const ellipse = new Elipse(TopicConfig.INNER_RECT_ATTRIBUTES);
@ -33,7 +34,7 @@ class ShirinkConnector {
ellipse.setFill('rgb(62,118,179)'); ellipse.setFill('rgb(62,118,179)');
ellipse.setSize(TopicConfig.CONNECTOR_WIDTH, TopicConfig.CONNECTOR_WIDTH); ellipse.setSize(TopicConfig.CONNECTOR_WIDTH, TopicConfig.CONNECTOR_WIDTH);
ellipse.addEvent('click', (event) => { ellipse.addEvent('click', (event: Event) => {
const model = topic.getModel(); const model = topic.getModel();
const collapse = !model.areChildrenShrunken(); const collapse = !model.areChildrenShrunken();

View File

@ -19,7 +19,7 @@ import $ from 'jquery';
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import { import {
Rect, Image, Line, Text, Group, ElementClass, Point Rect, Image, Line, Text, Group, ElementClass, Point,
} from '@wisemapping/web2d'; } from '@wisemapping/web2d';
import NodeGraph from './NodeGraph'; import NodeGraph from './NodeGraph';
@ -35,7 +35,6 @@ import NoteEditor from './widget/NoteEditor';
import ActionDispatcher from './ActionDispatcher'; import ActionDispatcher from './ActionDispatcher';
import LinkEditor from './widget/LinkEditor'; import LinkEditor from './widget/LinkEditor';
import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher'; import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher';
import { TopicShape } from './model/INodeModel'; import { TopicShape } from './model/INodeModel';
import NodeModel from './model/NodeModel'; import NodeModel from './model/NodeModel';
@ -50,14 +49,25 @@ const ICON_SCALING_FACTOR = 1.3;
abstract class Topic extends NodeGraph { abstract class Topic extends NodeGraph {
private _innerShape: ElementClass; private _innerShape: ElementClass;
private _relationships: Relationship[]; private _relationships: Relationship[];
private _isInWorkspace: boolean; private _isInWorkspace: boolean;
// eslint-disable-next-line no-use-before-define
private _children: Topic[]; private _children: Topic[];
// eslint-disable-next-line no-use-before-define
private _parent: Topic | null; private _parent: Topic | null;
private _outerShape: ElementClass; private _outerShape: ElementClass;
private _text: Text | null; private _text: Text | null;
private _iconsGroup: IconGroup; private _iconsGroup: IconGroup;
private _connector: any;
private _connector: ShirinkConnector;
private _outgoingLine: Line; private _outgoingLine: Line;
constructor(model: NodeModel, options) { constructor(model: NodeModel, options) {
@ -241,7 +251,7 @@ abstract class Topic extends NodeGraph {
result.setStroke(1, 'solid', stokeColor); result.setStroke(1, 'solid', stokeColor);
}; };
result.getSize = function getSize() { this.size }; result.getSize = function getSize() { return this.size; };
result.setPosition = () => { result.setPosition = () => {
// Overwrite behaviour ... // Overwrite behaviour ...
@ -1324,7 +1334,6 @@ abstract class Topic extends NodeGraph {
return result; return result;
} }
isChildTopic(childTopic: Topic): boolean { isChildTopic(childTopic: Topic): boolean {
let result = this.getId() === childTopic.getId(); let result = this.getId() === childTopic.getId();
if (!result) { if (!result) {

View File

@ -113,11 +113,11 @@ class Workspace {
} }
} }
addEvent(type: string, listener): void { addEvent(type: string, listener: (event: Event) => void): void {
this._workspace.addEvent(type, listener); this._workspace.addEvent(type, listener);
} }
removeEvent(type: string, listener): void { removeEvent(type: string, listener: (event: Event) => void): void {
$assert(type, 'type can not be null'); $assert(type, 'type can not be null');
$assert(listener, 'listener can not be null'); $assert(listener, 'listener can not be null');
this._workspace.removeEvent(type, listener); this._workspace.removeEvent(type, listener);
@ -193,7 +193,7 @@ class Workspace {
const workspace = this._workspace; const workspace = this._workspace;
const screenManager = this._screenManager; const screenManager = this._screenManager;
const mWorkspace = this; const mWorkspace = this;
const mouseDownListener = function mouseDownListener(event) { const mouseDownListener = function mouseDownListener(event: MouseEvent) {
if (!$defined(workspace._mouseMoveListener)) { if (!$defined(workspace._mouseMoveListener)) {
if (mWorkspace.isWorkspaceEventsEnabled()) { if (mWorkspace.isWorkspaceEventsEnabled()) {
mWorkspace.enableWorkspaceEvents(false); mWorkspace.enableWorkspaceEvents(false);
@ -202,7 +202,7 @@ class Workspace {
const originalCoordOrigin = workspace.getCoordOrigin(); const originalCoordOrigin = workspace.getCoordOrigin();
let wasDragged = false; let wasDragged = false;
workspace._mouseMoveListener = (mouseMoveEvent) => { workspace._mouseMoveListener = (mouseMoveEvent: MouseEvent) => {
const currentMousePosition = screenManager.getWorkspaceMousePosition(mouseMoveEvent); const currentMousePosition = screenManager.getWorkspaceMousePosition(mouseMoveEvent);
const offsetX = currentMousePosition.x - mouseDownPosition.x; const offsetX = currentMousePosition.x - mouseDownPosition.x;

View File

@ -18,6 +18,7 @@
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import Command from '../Command'; import Command from '../Command';
import CommandContext from '../CommandContext'; import CommandContext from '../CommandContext';
import FeatureModel from '../model/FeatureModel';
import FeatureType from '../model/FeatureType'; import FeatureType from '../model/FeatureType';
class AddFeatureToTopicCommand extends Command { class AddFeatureToTopicCommand extends Command {
@ -27,7 +28,7 @@ class AddFeatureToTopicCommand extends Command {
private _attributes: object; private _attributes: object;
private _featureModel: any; private _featureModel: FeatureModel;
/* /*
* @classdesc This command class handles do/undo of adding features to topics, e.g. an * @classdesc This command class handles do/undo of adding features to topics, e.g. an

View File

@ -24,9 +24,9 @@ class ChangeFeatureToTopicCommand extends Command {
private _topicId: number; private _topicId: number;
private _attributes: any; private _attributes;
constructor(topicId: number, featureId: number, attributes: any) { constructor(topicId: number, featureId: number, attributes) {
$assert($defined(topicId), 'topicId can not be null'); $assert($defined(topicId), 'topicId can not be null');
$assert($defined(featureId), 'featureId can not be null'); $assert($defined(featureId), 'featureId can not be null');
$assert($defined(attributes), 'attributes can not be null'); $assert($defined(attributes), 'attributes can not be null');
@ -53,7 +53,7 @@ class ChangeFeatureToTopicCommand extends Command {
* Overrides abstract parent method * Overrides abstract parent method
* @see {@link mindplot.Command.undoExecute} * @see {@link mindplot.Command.undoExecute}
*/ */
undoExecute(commandContext: any) { undoExecute(commandContext: CommandContext) {
this.execute(commandContext); this.execute(commandContext);
} }
} }

View File

@ -24,7 +24,7 @@ import Topic from '../Topic';
class DragTopicCommand extends Command { class DragTopicCommand extends Command {
private _topicsId: number; private _topicsId: number;
private _parentId: any; private _parentId: number;
private _position: Point; private _position: Point;
@ -62,7 +62,7 @@ class DragTopicCommand extends Command {
const origPosition = topic.getPosition(); const origPosition = topic.getPosition();
// Disconnect topic .. // Disconnect topic ..
if ($defined(origParentTopic) && origParentTopic !== this._parentId) { if ($defined(origParentTopic) && origParentTopic.getId() !== this._parentId) {
commandContext.disconnect(topic); commandContext.disconnect(topic);
} }
@ -76,9 +76,9 @@ class DragTopicCommand extends Command {
} }
// Finally, connect topic ... // Finally, connect topic ...
if (origParentTopic !== this._parentId) { if (!$defined(origParentTopic) || origParentTopic.getId() !== this._parentId) {
if ($defined(this._parentId)) { if ($defined(this._parentId)) {
const parentTopic = commandContext.findTopics(this._parentId)[0]; const parentTopic = commandContext.findTopics([this._parentId])[0];
commandContext.connect(topic, parentTopic); commandContext.connect(topic, parentTopic);
} }

View File

@ -21,8 +21,6 @@ import CommandContext from '../CommandContext';
import Topic from '../Topic'; import Topic from '../Topic';
class GenericFunctionCommand extends Command { class GenericFunctionCommand extends Command {
private _discardDuplicated: string;
private _value: string | object | boolean | number; private _value: string | object | boolean | number;
private _topicsId: number[]; private _topicsId: number[];
@ -42,7 +40,6 @@ class GenericFunctionCommand extends Command {
this._topicsId = topicsIds; this._topicsId = topicsIds;
this._commandFunc = commandFunc; this._commandFunc = commandFunc;
this._oldValues = []; this._oldValues = [];
this.discardDuplicated = undefined;
} }
/** /**
@ -79,14 +76,6 @@ class GenericFunctionCommand extends Command {
throw new Error('undo can not be applied.'); throw new Error('undo can not be applied.');
} }
} }
public get disardDuplicated(): string {
return this._discardDuplicated;
}
public set discardDuplicated(value: string) {
this._discardDuplicated = value;
}
} }
export default GenericFunctionCommand; export default GenericFunctionCommand;

View File

@ -16,13 +16,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import { Line } from '@wisemapping/web2d';
import Command from '../Command'; import Command from '../Command';
import ControlPoint from '../ControlPoint'; import ControlPoint from '../ControlPoint';
class MoveControlPointCommand extends Command { class MoveControlPointCommand extends Command {
private _ctrlPointControler: ControlPoint; private _ctrlPointControler: ControlPoint;
private _line: any; private _line: Line;
private _controlPoint: any; private _controlPoint: any;

View File

@ -18,13 +18,14 @@
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import Command from '../Command'; import Command from '../Command';
import CommandContext from '../CommandContext'; import CommandContext from '../CommandContext';
import FeatureModel from '../model/FeatureModel';
class RemoveFeatureFromTopicCommand extends Command { class RemoveFeatureFromTopicCommand extends Command {
private _topicId: number; private _topicId: number;
private _featureId: number; private _featureId: number;
private _oldFeature: any; private _oldFeature: FeatureModel;
/** /**
* @classdesc This command handles do/undo of removing a feature from a topic, e.g. an icon or * @classdesc This command handles do/undo of removing a feature from a topic, e.g. an icon or

View File

@ -16,9 +16,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import PositionType from '../PositionType';
import SizeType from '../SizeType';
class Node { class Node {
constructor(id, size, position, sorter) { private _id: number;
// eslint-disable-next-line no-use-before-define
_parent: Node;
private _sorter: any;
private _properties;
// eslint-disable-next-line no-use-before-define
_children: Node[];
constructor(id: number, size: SizeType, position, sorter) {
$assert(typeof id === 'number' && Number.isFinite(id), 'id can not be null'); $assert(typeof id === 'number' && Number.isFinite(id), 'id can not be null');
$assert(size, 'size can not be null'); $assert(size, 'size can not be null');
$assert(position, 'position can not be null'); $assert(position, 'position can not be null');
@ -69,7 +83,7 @@ class Node {
} }
/** */ /** */
setOrder(order) { setOrder(order: number) {
$assert( $assert(
typeof order === 'number' && Number.isFinite(order), typeof order === 'number' && Number.isFinite(order),
`Order can not be null. Value:${order}`, `Order can not be null. Value:${order}`,
@ -148,7 +162,7 @@ class Node {
y: oldDisplacement.y + displacement.y, y: oldDisplacement.y + displacement.y,
}; };
this._setProperty('freeDisplacement', Object.clone(newDisplacement)); this._setProperty('freeDisplacement', { ...newDisplacement });
} }
/** */ /** */
@ -163,7 +177,7 @@ class Node {
} }
/** */ /** */
setPosition(position) { setPosition(position: PositionType) {
$assert($defined(position), 'Position can not be null'); $assert($defined(position), 'Position can not be null');
$assert($defined(position.x), 'x can not be null'); $assert($defined(position.x), 'x can not be null');
$assert($defined(position.y), 'y can not be null'); $assert($defined(position.y), 'y can not be null');
@ -177,7 +191,7 @@ class Node {
) this._setProperty('position', position); ) this._setProperty('position', position);
} }
_setProperty(key, value) { _setProperty(key: string, value) {
let prop = this._properties[key]; let prop = this._properties[key];
if (!prop) { if (!prop) {
prop = { prop = {
@ -214,20 +228,13 @@ class Node {
/** @return {String} returns id, order, position, size and shrink information */ /** @return {String} returns id, order, position, size and shrink information */
toString() { toString() {
return ( return (
`[id:${ `[id:${this.getId()
this.getId() }, order:${this.getOrder()
}, order:${ }, position: {${this.getPosition().x
this.getOrder() },${this.getPosition().y
}, position: {${ }}, size: {${this.getSize().width
this.getPosition().x },${this.getSize().height
},${ }}, shrink:${this.areChildrenShrunken()
this.getPosition().y
}}, size: {${
this.getSize().width
},${
this.getSize().height
}}, shrink:${
this.areChildrenShrunken()
}]` }]`
); );
} }

View File

@ -16,8 +16,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { $assert, $defined } from '@wisemapping/core-js'; import { $assert, $defined } from '@wisemapping/core-js';
import PositionType from '../PositionType';
import Node from './Node';
class RootedTreeSet { class RootedTreeSet {
private _rootNodes: Node[];
protected _children: Node[];
constructor() { constructor() {
this._rootNodes = []; this._rootNodes = [];
} }
@ -26,7 +33,7 @@ class RootedTreeSet {
* @param root * @param root
* @throws will throw an error if root is null or undefined * @throws will throw an error if root is null or undefined
*/ */
setRoot(root) { setRoot(root: Node) {
$assert(root, 'root can not be null'); $assert(root, 'root can not be null');
this._rootNodes.push(this._decodate(root)); this._rootNodes.push(this._decodate(root));
} }
@ -36,8 +43,7 @@ class RootedTreeSet {
return this._rootNodes; return this._rootNodes;
} }
_decodate(node) { _decodate(node: Node) {
// eslint-disable-next-line no-param-reassign
node._children = []; node._children = [];
return node; return node;
} }
@ -48,7 +54,7 @@ class RootedTreeSet {
* @throws will throw an error if node with id already exists * @throws will throw an error if node with id already exists
* @throws will throw an error if node has been added already * @throws will throw an error if node has been added already
*/ */
add(node) { add(node: Node) {
$assert(node, 'node can not be null'); $assert(node, 'node can not be null');
$assert( $assert(
!this.find(node.getId(), false), !this.find(node.getId(), false),
@ -62,7 +68,7 @@ class RootedTreeSet {
* @param nodeId * @param nodeId
* @throws will throw an error if nodeId is null or undefined * @throws will throw an error if nodeId is null or undefined
*/ */
remove(nodeId) { remove(nodeId: number) {
$assert($defined(nodeId), 'nodeId can not be null'); $assert($defined(nodeId), 'nodeId can not be null');
const node = this.find(nodeId); const node = this.find(nodeId);
this._rootNodes = this._rootNodes.filter((n) => n !== node); this._rootNodes = this._rootNodes.filter((n) => n !== node);
@ -75,7 +81,7 @@ class RootedTreeSet {
* @throws will throw an error if childId is null or undefined * @throws will throw an error if childId is null or undefined
* @throws will throw an error if node with id childId is already a child of parent * @throws will throw an error if node with id childId is already a child of parent
*/ */
connect(parentId, childId) { connect(parentId: number, childId: number) {
$assert($defined(parentId), 'parent can not be null'); $assert($defined(parentId), 'parent can not be null');
$assert($defined(childId), 'child can not be null'); $assert($defined(childId), 'child can not be null');
@ -96,7 +102,7 @@ class RootedTreeSet {
* @throws will throw an error if nodeId is null or undefined * @throws will throw an error if nodeId is null or undefined
* @throws will throw an error if node is not connected * @throws will throw an error if node is not connected
*/ */
disconnect(nodeId) { disconnect(nodeId: number) {
$assert($defined(nodeId), 'nodeId can not be null'); $assert($defined(nodeId), 'nodeId can not be null');
const node = this.find(nodeId); const node = this.find(nodeId);
$assert(node._parent, 'Node is not connected'); $assert(node._parent, 'Node is not connected');
@ -113,7 +119,7 @@ class RootedTreeSet {
* @throws will throw an error if node cannot be found * @throws will throw an error if node cannot be found
* @return node * @return node
*/ */
find(id, validate = true) { find(id: number, validate = true): Node {
$assert($defined(id), 'id can not be null'); $assert($defined(id), 'id can not be null');
const graphs = this._rootNodes; const graphs = this._rootNodes;
@ -132,7 +138,7 @@ class RootedTreeSet {
return result; return result;
} }
_find(id, parent) { private _find(id: number, parent: Node): Node {
if (parent.getId() === id) { if (parent.getId() === id) {
return parent; return parent;
} }
@ -153,7 +159,7 @@ class RootedTreeSet {
* @throws will throw an error if nodeId is null or undefined * @throws will throw an error if nodeId is null or undefined
* @return children * @return children
*/ */
getChildren(node) { getChildren(node: Node): Node[] {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
return node._children; return node._children;
} }
@ -163,7 +169,7 @@ class RootedTreeSet {
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return root node or the provided node, if it has no parent * @return root node or the provided node, if it has no parent
*/ */
getRootNode(node) { getRootNode(node: Node) {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
const parent = this.getParent(node); const parent = this.getParent(node);
if ($defined(parent)) { if ($defined(parent)) {
@ -177,12 +183,12 @@ class RootedTreeSet {
* @param node * @param node
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return {Array} ancestors */ * @return {Array} ancestors */
getAncestors(node) { getAncestors(node: Node): Node[] {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
return this._getAncestors(this.getParent(node), []); return this._getAncestors(this.getParent(node), []);
} }
_getAncestors(node, ancestors) { _getAncestors(node: Node, ancestors: Node[]) {
const result = ancestors; const result = ancestors;
if (node) { if (node) {
result.push(node); result.push(node);
@ -196,7 +202,7 @@ class RootedTreeSet {
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return {Array} siblings * @return {Array} siblings
*/ */
getSiblings(node) { getSiblings(node: Node): Node[] {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
if (!$defined(node._parent)) { if (!$defined(node._parent)) {
return []; return [];
@ -210,12 +216,12 @@ class RootedTreeSet {
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return {Boolean} whether the node has a single path to a single leaf (no branching) * @return {Boolean} whether the node has a single path to a single leaf (no branching)
*/ */
hasSinglePathToSingleLeaf(node) { hasSinglePathToSingleLeaf(node: Node): boolean {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
return this._hasSinglePathToSingleLeaf(node); return this._hasSinglePathToSingleLeaf(node);
} }
_hasSinglePathToSingleLeaf(node) { private _hasSinglePathToSingleLeaf(node: Node): boolean {
const children = this.getChildren(node); const children = this.getChildren(node);
if (children.length === 1) { if (children.length === 1) {
@ -228,7 +234,7 @@ class RootedTreeSet {
/** /**
* @param node * @param node
* @return {Boolean} whether the node is the start of a subbranch */ * @return {Boolean} whether the node is the start of a subbranch */
isStartOfSubBranch(node) { isStartOfSubBranch(node: Node): boolean {
return this.getSiblings(node).length > 0 && this.getChildren(node).length === 1; return this.getSiblings(node).length > 0 && this.getChildren(node).length === 1;
} }
@ -237,7 +243,7 @@ class RootedTreeSet {
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return {Boolean} whether the node is a leaf * @return {Boolean} whether the node is a leaf
*/ */
isLeaf(node) { isLeaf(node: Node): boolean {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
return this.getChildren(node).length === 0; return this.getChildren(node).length === 0;
} }
@ -247,7 +253,7 @@ class RootedTreeSet {
* @throws will throw an error if node is null or undefined * @throws will throw an error if node is null or undefined
* @return parent * @return parent
*/ */
getParent(node) { getParent(node: Node): Node {
$assert(node, 'node cannot be null'); $assert(node, 'node cannot be null');
return node._parent; return node._parent;
} }
@ -265,7 +271,7 @@ class RootedTreeSet {
return result; return result;
} }
_dump(node, indent) { _dump(node: Node, indent: string) {
let result = `${indent + node}\n`; let result = `${indent + node}\n`;
const children = this.getChildren(node); const children = this.getChildren(node);
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
@ -287,7 +293,7 @@ class RootedTreeSet {
} }
} }
_plot(canvas, node, root) { _plot(canvas, node: Node, root?) {
const children = this.getChildren(node); const children = this.getChildren(node);
const cx = node.getPosition().x + canvas.width / 2 - node.getSize().width / 2; const cx = node.getPosition().x + canvas.width / 2 - node.getSize().width / 2;
const cy = node.getPosition().y + canvas.height / 2 - node.getSize().height / 2; const cy = node.getPosition().y + canvas.height / 2 - node.getSize().height / 2;
@ -316,43 +322,27 @@ class RootedTreeSet {
const rectSize = { width: rect.attr('width'), height: rect.attr('height') }; const rectSize = { width: rect.attr('width'), height: rect.attr('height') };
rect.click(() => { rect.click(() => {
console.log( console.log(
`[id:${ `[id:${node.getId()
node.getId() }, order:${node.getOrder()
}, order:${ }, position:(${rectPosition.x
node.getOrder() }, ${rectPosition.y
}, position:(${ }), size:${rectSize.width
rectPosition.x },${rectSize.height
}, ${ }, freeDisplacement:(${node.getFreeDisplacement().x
rectPosition.y },${node.getFreeDisplacement().y
}), size:${
rectSize.width
},${
rectSize.height
}, freeDisplacement:(${
node.getFreeDisplacement().x
},${
node.getFreeDisplacement().y
})]`, })]`,
); );
}); });
text.click(() => { text.click(() => {
console.log( console.log(
`[id:${ `[id:${node.getId()
node.getId() }, order:${node.getOrder()
}, order:${ }, position:(${rectPosition.x
node.getOrder() },${rectPosition.y
}, position:(${ }), size:${rectSize.width
rectPosition.x }x${rectSize.height
},${ }, freeDisplacement:(${node.getFreeDisplacement().x
rectPosition.y },${node.getFreeDisplacement().y
}), size:${
rectSize.width
}x${
rectSize.height
}, freeDisplacement:(${
node.getFreeDisplacement().x
},${
node.getFreeDisplacement().y
})]`, })]`,
); );
}); });
@ -367,7 +357,7 @@ class RootedTreeSet {
* @param node * @param node
* @param position * @param position
*/ */
updateBranchPosition(node, position) { updateBranchPosition(node: Node, position: PositionType): void {
const oldPos = node.getPosition(); const oldPos = node.getPosition();
node.setPosition(position); node.setPosition(position);
@ -386,7 +376,7 @@ class RootedTreeSet {
* @param xOffset * @param xOffset
* @param yOffset * @param yOffset
*/ */
shiftBranchPosition(node, xOffset, yOffset) { shiftBranchPosition(node: Node, xOffset: number, yOffset: number): void {
const position = node.getPosition(); const position = node.getPosition();
node.setPosition({ x: position.x + xOffset, y: position.y + yOffset }); node.setPosition({ x: position.x + xOffset, y: position.y + yOffset });
@ -402,7 +392,7 @@ class RootedTreeSet {
* @param yOffset * @param yOffset
* @return siblings in the offset (vertical) direction, i.e. with lower or higher order * @return siblings in the offset (vertical) direction, i.e. with lower or higher order
*/ */
getSiblingsInVerticalDirection(node, yOffset) { getSiblingsInVerticalDirection(node: Node, yOffset: number): Node[] {
// siblings with lower or higher order // siblings with lower or higher order
// (depending on the direction of the offset and on the same side as their parent) // (depending on the direction of the offset and on the same side as their parent)
const parent = this.getParent(node); const parent = this.getParent(node);
@ -429,7 +419,7 @@ class RootedTreeSet {
* @return branches of the root node on the same side as the given node's, in the given * @return branches of the root node on the same side as the given node's, in the given
* vertical direction * vertical direction
*/ */
getBranchesInVerticalDirection(node, yOffset) { getBranchesInVerticalDirection(node: Node, yOffset: number): Node[] {
// direct descendants of the root that do not contain the node and are on the same side // direct descendants of the root that do not contain the node and are on the same side
// and on the direction of the offset // and on the direction of the offset
const rootNode = this.getRootNode(node); const rootNode = this.getRootNode(node);
@ -437,7 +427,7 @@ class RootedTreeSet {
.filter(((child) => this._find(node.getId(), child))); .filter(((child) => this._find(node.getId(), child)));
const branch = branches[0]; const branch = branches[0];
const rootDescendants = this.getSiblings(branch).filter((sibling) => { const result = this.getSiblings(branch).filter((sibling) => {
const sameSide = node.getPosition().x > rootNode.getPosition().x const sameSide = node.getPosition().x > rootNode.getPosition().x
? sibling.getPosition().x > rootNode.getPosition().x ? sibling.getPosition().x > rootNode.getPosition().x
: sibling.getPosition().x < rootNode.getPosition().x; : sibling.getPosition().x < rootNode.getPosition().x;
@ -447,7 +437,7 @@ class RootedTreeSet {
return sameSide && sameDirection; return sameSide && sameDirection;
}, this); }, this);
return rootDescendants; return result;
} }
} }

View File

@ -112,7 +112,7 @@ class BootstrapDialog extends Options {
return header; return header;
} }
onAcceptClick(event) { onAcceptClick() {
throw new Error('Unsupported operation'); throw new Error('Unsupported operation');
} }
@ -120,7 +120,7 @@ class BootstrapDialog extends Options {
// Overwrite default behaviour ... // Overwrite default behaviour ...
} }
onRemoveClick(event) { onRemoveClick() {
throw new Error('Unsupported operation'); throw new Error('Unsupported operation');
} }

View File

@ -37,8 +37,7 @@ const queryClient = new QueryClient({
}); });
const App = (): ReactElement => { const App = (): ReactElement => {
const appi18n = new AppI18n(); const locale = AppI18n.getBrowserLocale();
const locale = appi18n.getBrowserLocale();
// global variables set server-side // global variables set server-side
const istTryMode = global.memoryPersistence; const istTryMode = global.memoryPersistence;

View File

@ -15,13 +15,13 @@ export class Locale {
} }
} }
export default class AppI18n { export default abstract class AppI18n {
public getUserLocale(): Locale { public static getUserLocale(): Locale {
const account = fetchAccount(); const account = fetchAccount();
return account ? account.locale : this.getBrowserLocale(); return account?.locale ? account.locale : this.getBrowserLocale();
} }
public getBrowserLocale(): Locale { public static getBrowserLocale(): Locale {
let localeCode = (navigator.languages && navigator.languages[0]) || navigator.language; let localeCode = (navigator.languages && navigator.languages[0]) || navigator.language;
// Just remove the variant ... // Just remove the variant ...

View File

@ -90,10 +90,22 @@ class CacheDecoratorClient implements Client {
return this.client.fetchLabels(); return this.client.fetchLabels();
} }
createLabel(title: string, color: string): Promise<number> {
return this.client.createLabel(title, color);
}
deleteLabel(id: number): Promise<void> { deleteLabel(id: number): Promise<void> {
return this.client.deleteLabel(id); return this.client.deleteLabel(id);
} }
addLabelToMap(labelId: number, mapId: number): Promise<void> {
return this.client.addLabelToMap(labelId, mapId);
}
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
return this.client.deleteLabelFromMap(labelId, mapId);
}
fetchAccountInfo(): Promise<AccountInfo> { fetchAccountInfo(): Promise<AccountInfo> {
return this.client.fetchAccountInfo(); return this.client.fetchAccountInfo();
} }

View File

@ -63,7 +63,7 @@ export type AccountInfo = {
firstname: string; firstname: string;
lastname: string; lastname: string;
email: string; email: string;
locale: Locale; locale?: Locale;
}; };
export type Permission = { export type Permission = {
@ -94,8 +94,11 @@ interface Client {
updateStarred(id: number, starred: boolean): Promise<void>; updateStarred(id: number, starred: boolean): Promise<void>;
updateMapToPublic(id: number, isPublic: boolean): Promise<void>; updateMapToPublic(id: number, isPublic: boolean): Promise<void>;
createLabel(title: string, color: string): Promise<number>;
fetchLabels(): Promise<Label[]>; fetchLabels(): Promise<Label[]>;
deleteLabel(id: number): Promise<void>; deleteLabel(id: number): Promise<void>;
addLabelToMap(labelId: number, mapId: number): Promise<void>;
deleteLabelFromMap(labelId: number, mapId: number): Promise<void>;
fetchAccountInfo(): Promise<AccountInfo>; fetchAccountInfo(): Promise<AccountInfo>;
registerNewUser(user: NewUser): Promise<void>; registerNewUser(user: NewUser): Promise<void>;

View File

@ -327,9 +327,46 @@ class MockClient implements Client {
} }
} }
createLabel(title: string, color: string): Promise<number> {
const newId = Math.max.apply(Number, this.labels.map(l => l.id)) + 1;
this.labels.push({
id: newId,
title,
color,
});
return newId;
}
deleteLabel(id: number): Promise<void> { deleteLabel(id: number): Promise<void> {
this.labels = this.labels.filter((l) => l.id != id); this.labels = this.labels.filter((l) => l.id != id);
console.log('Label delete:' + this.labels); this.maps = this.maps.map(m => {
return {
...m,
labels: m.labels.filter((l) => l.id != id)
};
});
return Promise.resolve();
}
addLabelToMap(labelId: number, mapId: number): Promise<void> {
const labelToAdd = this.labels.find((l) => l.id === labelId);
if (!labelToAdd) {
return Promise.reject({ msg: `unable to find label with id ${labelId}`});
}
const map = this.maps.find((m) => m.id === mapId);
if (!map) {
return Promise.reject({ msg: `unable to find map with id ${mapId}` });
}
map.labels.push(labelToAdd);
return Promise.resolve();
}
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
const map = this.maps.find((m) => m.id === mapId);
if (!map) {
return Promise.reject({ msg: `unable to find map with id ${mapId}` });
}
map.labels = map.labels.filter((l) => l.id !== labelId);
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -11,7 +11,7 @@ import Client, {
ImportMapInfo, ImportMapInfo,
Permission, Permission,
} from '..'; } from '..';
import { LocaleCode, localeFromStr, Locales } from '../../app-i18n'; import { LocaleCode, localeFromStr } from '../../app-i18n';
export default class RestClient implements Client { export default class RestClient implements Client {
private baseUrl: string; private baseUrl: string;
@ -184,7 +184,7 @@ export default class RestClient implements Client {
`${this.baseUrl}/c/restful/maps?title=${model.title}&description=${model.description ? model.description : '' `${this.baseUrl}/c/restful/maps?title=${model.title}&description=${model.description ? model.description : ''
}`, }`,
model.content, model.content,
{ headers: { 'Content-Type': model.contentType } } { headers: { 'Content-Type': 'application/xml' } }
) )
.then((response) => { .then((response) => {
const mapId = response.headers.resourceid; const mapId = response.headers.resourceid;
@ -214,7 +214,7 @@ export default class RestClient implements Client {
lastname: account.lastname ? account.lastname : '', lastname: account.lastname ? account.lastname : '',
firstname: account.firstname ? account.firstname : '', firstname: account.firstname ? account.firstname : '',
email: account.email, email: account.email,
locale: locale ? localeFromStr(locale) : Locales.EN, locale: locale ? localeFromStr(locale) : undefined,
}); });
}) })
.catch((error) => { .catch((error) => {
@ -510,10 +510,59 @@ export default class RestClient implements Client {
return new Promise(handler); return new Promise(handler);
} }
createLabel(title: string, color: string): Promise<number> {
const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => {
axios
.post(`${this.baseUrl}/c/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), {
headers: { 'Content-Type': 'application/json' },
})
.then((response) => {
success(response.headers.resourceid);
})
.catch((error) => {
const errorInfo = this.parseResponseOnError(error.response);
reject(errorInfo);
});
};
return new Promise(handler);
}
deleteLabel(id: number): Promise<void> { deleteLabel(id: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios axios
.delete(`${this.baseUrl}/c/restful/label/${id}`) .delete(`${this.baseUrl}/c/restful/labels/${id}`)
.then(() => {
success();
})
.catch((error) => {
const errorInfo = this.parseResponseOnError(error.response);
reject(errorInfo);
});
};
return new Promise(handler);
}
addLabelToMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
.post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), {
headers: { 'Content-Type': 'application/json' },
})
.then(() => {
success();
})
.catch((error) => {
const errorInfo = this.parseResponseOnError(error.response);
reject(errorInfo);
});
};
return new Promise(handler);
}
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
.delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`)
.then(() => { .then(() => {
success(); success();
}) })

View File

@ -13,8 +13,8 @@ const EditorPage = ({ mapId, ...props }: EditorPropsType): React.ReactElement =>
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null); const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null);
// Load user locale ... // Load user locale ...
const appi18n = new AppI18n(); const userLocale = AppI18n.getUserLocale();
const userLocale = appi18n.getUserLocale(); console.log("Locale:" + userLocale.code);
return <> return <>
<Editor {...props} onAction={setActiveDialog} locale={userLocale.code} /> <Editor {...props} onAction={setActiveDialog} locale={userLocale.code} />

View File

@ -13,6 +13,8 @@ type InputProps = {
autoComplete?: string; autoComplete?: string;
fullWidth?: boolean; fullWidth?: boolean;
disabled?: boolean; disabled?: boolean;
maxLength?: number,
rows?: number
}; };
const Input = ({ const Input = ({
@ -26,6 +28,7 @@ const Input = ({
autoComplete, autoComplete,
fullWidth = true, fullWidth = true,
disabled = false, disabled = false,
maxLength = 254,
}: InputProps): React.ReactElement => { }: InputProps): React.ReactElement => {
const fieldError = error?.fields?.[name]; const fieldError = error?.fields?.[name];
return ( return (
@ -43,6 +46,7 @@ const Input = ({
margin="dense" margin="dense"
disabled={disabled} disabled={disabled}
autoComplete={autoComplete} autoComplete={autoComplete}
inputProps={{ maxLength: maxLength }}
/> />
); );
}; };

View File

@ -86,6 +86,7 @@ const CreateDialog = ({ onClose }: CreateProps): React.ReactElement => {
onChange={handleOnChange} onChange={handleOnChange}
error={error} error={error}
fullWidth={true} fullWidth={true}
maxLength={60}
/> />
<Input <Input
@ -99,6 +100,7 @@ const CreateDialog = ({ onClose }: CreateProps): React.ReactElement => {
onChange={handleOnChange} onChange={handleOnChange}
required={false} required={false}
fullWidth={true} fullWidth={true}
rows={3}
/> />
</FormControl> </FormControl>
</BaseDialog> </BaseDialog>

View File

@ -4,20 +4,15 @@ import { useMutation, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Client from '../../../../classes/client'; import Client from '../../../../classes/client';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { handleOnMutationSuccess } from '..'; import { handleOnMutationSuccess, MultiDialogProps } from '..';
import BaseDialog from '../base-dialog'; import BaseDialog from '../base-dialog';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle'; import AlertTitle from '@mui/material/AlertTitle';
export type DeleteMultiselectDialogProps = {
mapsId: number[];
onClose: () => void;
};
const DeleteMultiselectDialog = ({ const DeleteMultiselectDialog = ({
onClose, onClose,
mapsId, mapsId,
}: DeleteMultiselectDialogProps): React.ReactElement => { }: MultiDialogProps): React.ReactElement => {
const intl = useIntl(); const intl = useIntl();
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@ -210,15 +210,15 @@ const ExportDialog = ({
value={exportFormat} value={exportFormat}
className={classes.select} className={classes.select}
> >
<MenuItem className={classes.select} value="xls">
Microsoft Excel (XLS)
</MenuItem>
<MenuItem className={classes.select} value="txt"> <MenuItem className={classes.select} value="txt">
Plain Text File (TXT) Plain Text File (TXT)
</MenuItem> </MenuItem>
<MenuItem className={classes.select} value="md"> <MenuItem className={classes.select} value="md">
Markdown (MD) Markdown (MD)
</MenuItem> </MenuItem>
{/* <MenuItem className={classes.select} value="xls">
Microsoft Excel (XLS)
</MenuItem> */}
</Select> </Select>
)} )}
</FormControl> </FormControl>
@ -248,9 +248,9 @@ const ExportDialog = ({
<MenuItem className={classes.select} value="mm"> <MenuItem className={classes.select} value="mm">
Freemind 1.0.1 (MM) Freemind 1.0.1 (MM)
</MenuItem> </MenuItem>
<MenuItem className={classes.select} value="mmap"> {/* <MenuItem className={classes.select} value="mmap">
MindManager (MMAP) MindManager (MMAP)
</MenuItem> </MenuItem> */}
</Select> </Select>
)} )}
</FormControl> </FormControl>

View File

@ -105,7 +105,7 @@ const ImportDialog = ({ onClose }: CreateProps): React.ReactElement => {
description={intl.formatMessage({ description={intl.formatMessage({
id: 'import.description', id: 'import.description',
defaultMessage: defaultMessage:
'You can import FreeMind 1.0.1 and WiseMapping maps to your list of maps. Select the file you want to import.', 'You can import WiseMapping maps to your list of maps. Select the file you want to import.',
})} })}
submitButton={intl.formatMessage({ id: 'import.button', defaultMessage: 'Create' })} submitButton={intl.formatMessage({ id: 'import.button', defaultMessage: 'Create' })}
> >

View File

@ -12,6 +12,7 @@ import InfoDialog from './info-dialog';
import DeleteMultiselectDialog from './delete-multiselect-dialog'; import DeleteMultiselectDialog from './delete-multiselect-dialog';
import ExportDialog from './export-dialog'; import ExportDialog from './export-dialog';
import ShareDialog from './share-dialog'; import ShareDialog from './share-dialog';
import LabelDialog from './label-dialog';
export type BasicMapInfo = { export type BasicMapInfo = {
name: string; name: string;
@ -61,6 +62,7 @@ const ActionDispatcher = ({ mapsId, action, onClose, fromEditor }: ActionDialogP
<ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} /> <ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} />
)} )}
{action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />} {action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />}
{action === 'label' && <LabelDialog onClose={handleOnClose} mapsId={mapsId} />}
</span> </span>
); );
}; };
@ -79,4 +81,9 @@ export type SimpleDialogProps = {
onClose: () => void; onClose: () => void;
}; };
export type MultiDialogProps = {
mapsId: number[];
onClose: () => void;
};
export default ActionDispatcher; export default ActionDispatcher;

View File

@ -0,0 +1,76 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { FormattedMessage, useIntl } from 'react-intl';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import Typography from '@mui/material/Typography';
import { useStyles } from './style';
import { MultiDialogProps } from '..';
import BaseDialog from '../base-dialog';
import Client, { ErrorInfo, Label, MapInfo } from '../../../../classes/client';
import { LabelSelector } from '../../maps-list/label-selector';
import { activeInstance } from '../../../../redux/clientSlice';
import { ChangeLabelMutationFunctionParam, getChangeLabelMutationFunction } from '../../maps-list';
const LabelDialog = ({ mapsId, onClose }: MultiDialogProps): React.ReactElement => {
const intl = useIntl();
const classes = useStyles();
const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient();
// TODO: pass down map data instead of using query?
const { data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', () => {
return client.fetchAllMaps();
});
const maps = data.filter(m => mapsId.includes(m.id));
const changeLabelMutation = useMutation<void, ErrorInfo, ChangeLabelMutationFunctionParam, number>(
getChangeLabelMutationFunction(client),
{
onSuccess: () => {
queryClient.invalidateQueries('maps');
queryClient.invalidateQueries('labels');
},
onError: (error) => {
console.error(error);
}
}
);
const handleChangesInLabels = (label: Label, checked: boolean) => {
changeLabelMutation.mutate({
maps,
label,
checked
});
};
return (
<div>
<BaseDialog
onClose={onClose}
title={intl.formatMessage({
id: 'label.title',
defaultMessage: 'Add a label',
})}
description={intl.formatMessage({
id: 'label.description',
defaultMessage:
'Use labels to organize your maps.',
})}
PaperProps={{ classes: { root: classes.paper } }}
>
<>
<Typography variant="body2" marginTop="10px">
<FormattedMessage id="label.add-for" defaultMessage="Editing labels for maps: " />
{ maps.map(m => m.title).join(', ') }
</Typography>
<LabelSelector onChange={handleChangesInLabels} maps={maps} />
</>
</BaseDialog>
</div>);
};
export default LabelDialog;

View File

@ -0,0 +1,10 @@
import createStyles from '@mui/styles/createStyles';
import makeStyles from '@mui/styles/makeStyles';
export const useStyles = makeStyles(() =>
createStyles({
paper: {
maxWidth: '420px',
},
})
);

View File

@ -7,7 +7,7 @@ import List from '@mui/material/List';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import { useStyles } from './style'; import { useStyles } from './style';
import { MapsList } from './maps-list'; import { MapsList } from './maps-list';
import { createIntl, createIntlCache, FormattedMessage, IntlProvider, IntlShape, useIntl } from 'react-intl'; import { createIntl, createIntlCache, FormattedMessage, IntlProvider } from 'react-intl';
import { useQuery, useMutation, useQueryClient } from 'react-query'; import { useQuery, useMutation, useQueryClient } from 'react-query';
import { activeInstance } from '../../redux/clientSlice'; import { activeInstance } from '../../redux/clientSlice';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -40,6 +40,7 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import logoIcon from './logo-small.svg'; import logoIcon from './logo-small.svg';
import poweredByIcon from './pwrdby-white.svg'; import poweredByIcon from './pwrdby-white.svg';
import LabelDeleteConfirm from './maps-list/label-delete-confirm';
export type Filter = GenericFilter | LabelFilter; export type Filter = GenericFilter | LabelFilter;
@ -64,10 +65,9 @@ const MapsPage = (): ReactElement => {
const client: Client = useSelector(activeInstance); const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined); const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null);
// Reload based on user preference ... // Reload based on user preference ...
const appi18n = new AppI18n(); const userLocale = AppI18n.getUserLocale();
const userLocale = appi18n.getUserLocale();
const cache = createIntlCache(); const cache = createIntlCache();
const intl = createIntl({ const intl = createIntl({
@ -77,7 +77,6 @@ const MapsPage = (): ReactElement => {
}, cache) }, cache)
useEffect(() => { useEffect(() => {
document.title = intl.formatMessage({ document.title = intl.formatMessage({
id: 'maps.page-title', id: 'maps.page-title',
defaultMessage: 'My Maps | WiseMapping', defaultMessage: 'My Maps | WiseMapping',
@ -85,7 +84,10 @@ const MapsPage = (): ReactElement => {
}, []); }, []);
const mutation = useMutation((id: number) => client.deleteLabel(id), { const mutation = useMutation((id: number) => client.deleteLabel(id), {
onSuccess: () => queryClient.invalidateQueries('labels'), onSuccess: () => {
queryClient.invalidateQueries('labels');
queryClient.invalidateQueries('maps');
},
onError: (error) => { onError: (error) => {
console.error(`Unexpected error ${error}`); console.error(`Unexpected error ${error}`);
}, },
@ -238,7 +240,7 @@ const MapsPage = (): ReactElement => {
filter={buttonInfo.filter} filter={buttonInfo.filter}
active={filter} active={filter}
onClick={handleMenuClick} onClick={handleMenuClick}
onDelete={handleLabelDelete} onDelete={setLabelToDelete}
key={`${buttonInfo.filter.type}:${buttonInfo.label}`} key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
/> />
); );
@ -259,6 +261,14 @@ const MapsPage = (): ReactElement => {
<MapsList filter={filter} /> <MapsList filter={filter} />
</main> </main>
</div> </div>
{ labelToDelete && <LabelDeleteConfirm
onClose={() => setLabelToDelete(null)}
onConfirm={() => {
handleLabelDelete(labelToDelete);
setLabelToDelete(null);
}}
label={labels.find(l => l.id === labelToDelete)}
/> }
</IntlProvider> </IntlProvider>
); );
}; };

View File

@ -3,9 +3,9 @@ import React from 'react';
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import Client from '../../../classes/client'; import Client from '../../../classes/client';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { activeInstance, fetchAccount } from '../../../redux/clientSlice'; import { activeInstance } from '../../../redux/clientSlice';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { LocaleCode, Locales } from '../../../classes/app-i18n'; import AppI18n, { LocaleCode, Locales } from '../../../classes/app-i18n';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
@ -49,7 +49,7 @@ const LanguageMenu = (): React.ReactElement => {
mutation.mutate(localeCode); mutation.mutate(localeCode);
}; };
const accountInfo = fetchAccount(); const userLocale = AppI18n.getUserLocale();
return ( return (
<span> <span>
<Tooltip <Tooltip
@ -68,7 +68,7 @@ const LanguageMenu = (): React.ReactElement => {
onClick={handleMenu} onClick={handleMenu}
startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />} startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />}
> >
{accountInfo?.locale?.label} {userLocale.label}
</Button> </Button>
</Tooltip> </Tooltip>
<Menu <Menu

View File

@ -1,71 +0,0 @@
import React from 'react';
import Popover from '@mui/material/Popover';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import { FormattedMessage, useIntl } from 'react-intl';
import { Label } from '../../../../classes/client';
import { LabelSelector } from '../label-selector';
type AddLabelButtonTypes = {
onChange?: (label: Label) => void;
};
export function AddLabelButton({ onChange }: AddLabelButtonTypes): React.ReactElement {
console.log(onChange);
const intl = useIntl();
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'add-label-popover' : undefined;
return (
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'map.tooltip-add',
defaultMessage: 'Add label to selected',
})}
>
<>
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
style={{ marginLeft: '10px' }}
disableElevation={true}
startIcon={<LabelTwoTone />}
onClick={handleClick}
>
<FormattedMessage id="action.label" defaultMessage="Add Label" />
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<LabelSelector />
</Popover>
</>
</Tooltip>
);
}

View File

@ -0,0 +1,94 @@
import React from 'react';
import { useIntl } from 'react-intl';
import AddIcon from '@mui/icons-material/Add';
import TextField from '@mui/material/TextField';
import { Label } from '../../../../classes/client';
import { StyledButton, NewLabelContainer, NewLabelColor, CreateLabel } from './styled';
import { Tooltip } from '@mui/material';
const labelColors = [
'#00b327',
'#0565ff',
'#2d2dd6',
'#6a00ba',
'#ad1599',
'#ff1e35',
'#ff6600',
'#ffff47',
];
type AddLabelFormProps = {
onAdd: (newLabel: Label) => void;
};
export default function AddLabelForm({ onAdd }: AddLabelFormProps): React.ReactElement {
const intl = useIntl();
const [createLabelColorIndex, setCreateLabelColorIndex] = React.useState(
Math.floor(Math.random() * labelColors.length)
);
const [newLabelTitle, setNewLabelTitle] = React.useState('');
const newLabelColor = labelColors[createLabelColorIndex];
const setNextLabelColorIndex = () => {
const nextIndex = labelColors[createLabelColorIndex + 1] ? createLabelColorIndex + 1 : 0;
setCreateLabelColorIndex(nextIndex);
};
const handleSubmitNew = () => {
onAdd({
title: newLabelTitle,
color: newLabelColor,
id: 0,
});
setNewLabelTitle('');
setNextLabelColorIndex();
};
return (
<CreateLabel>
<NewLabelContainer>
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'label.change-color',
defaultMessage: 'Change label color',
})}
>
<NewLabelColor
htmlColor={newLabelColor}
onClick={(e) => {
e.stopPropagation();
setNextLabelColorIndex();
}}
/>
</Tooltip>
<TextField
variant="standard"
label={intl.formatMessage({
id: 'label.add-placeholder',
defaultMessage: 'Label title',
})}
onChange={(e) => setNewLabelTitle(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSubmitNew();
}
}}
value={newLabelTitle}
/>
<StyledButton
onClick={() => handleSubmitNew()}
disabled={!newLabelTitle.length}
aria-label={intl.formatMessage({
id: 'label.add-button',
defaultMessage: 'Add label',
})}
>
<AddIcon />
</StyledButton>
</NewLabelContainer>
</CreateLabel>
);
}

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
import IconButton from '@mui/material/IconButton';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
export const StyledButton = styled(IconButton)`
margin: 4px;
`;
export const NewLabelContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;
export const NewLabelColor = styled(LabelTwoTone)`
margin-right: 12px;
cursor: pointer;
`;
export const CreateLabel = styled.div`
padding-top: 10px;
display: flex;
flex-direction: column;
justify-content: flex-end;
`;

View File

@ -2,7 +2,7 @@ import React, { useEffect, CSSProperties } from 'react';
import { useStyles } from './styled'; import { useStyles } from './styled';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { activeInstance, fetchAccount } from '../../../redux/clientSlice'; import { activeInstance } from '../../../redux/clientSlice';
import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useMutation, useQuery, useQueryClient } from 'react-query';
import Client, { ErrorInfo, Label, MapInfo } from '../../../classes/client'; import Client, { ErrorInfo, Label, MapInfo } from '../../../classes/client';
import ActionChooser, { ActionType } from '../action-chooser'; import ActionChooser, { ActionType } from '../action-chooser';
@ -33,12 +33,13 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded'; import StarRateRoundedIcon from '@mui/icons-material/StarRateRounded';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { AddLabelButton } from './add-label-button';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { LabelsCell } from './labels-cell'; import { LabelsCell } from './labels-cell';
import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import AppI18n from '../../../classes/app-i18n';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
dayjs.extend(LocalizedFormat) dayjs.extend(LocalizedFormat);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) { function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
@ -235,6 +236,24 @@ const mapsFilter = (filter: Filter, search: string): ((mapInfo: MapInfo) => bool
}; };
}; };
export type ChangeLabelMutationFunctionParam = { maps: MapInfo[]; label: Label; checked: boolean };
export const getChangeLabelMutationFunction =
(client: Client) =>
async ({ maps, label, checked }: ChangeLabelMutationFunctionParam): Promise<void> => {
if (!label.id) {
label.id = await client.createLabel(label.title, label.color);
}
if (checked) {
const toAdd = maps.filter((m) => !m.labels.find((l) => l.id === label.id));
await Promise.all(toAdd.map((m) => client.addLabelToMap(label.id, m.id)));
} else {
const toRemove = maps.filter((m) => m.labels.find((l) => l.id === label.id));
await Promise.all(toRemove.map((m) => client.deleteLabelFromMap(label.id, m.id)));
}
return Promise.resolve();
};
export const MapsList = (props: MapsListProps): React.ReactElement => { export const MapsList = (props: MapsListProps): React.ReactElement => {
const classes = useStyles(); const classes = useStyles();
const [order, setOrder] = React.useState<Order>('desc'); const [order, setOrder] = React.useState<Order>('desc');
@ -251,10 +270,8 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Configure locale ... // Configure locale ...
const account = fetchAccount(); const userLocale = AppI18n.getUserLocale();
if (account) { dayjs.locale(userLocale.code);
dayjs.locale(account.locale.code);
}
useEffect(() => { useEffect(() => {
setSelected([]); setSelected([]);
@ -331,7 +348,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
event.stopPropagation(); event.stopPropagation();
}; };
}; };
9;
const starredMultation = useMutation<void, ErrorInfo, number>( const starredMultation = useMutation<void, ErrorInfo, number>(
(id: number) => { (id: number) => {
const map = mapsInfo.find((m) => m.id == id); const map = mapsInfo.find((m) => m.id == id);
@ -385,6 +402,36 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
}); });
}; };
const handleAddLabelClick = () => {
setActiveDialog({
actionType: 'label',
mapsId: selected,
});
};
const removeLabelMultation = useMutation<
void,
ErrorInfo,
{ mapId: number; labelId: number },
number
>(
({ mapId, labelId }) => {
return client.deleteLabelFromMap(labelId, mapId);
},
{
onSuccess: () => {
queryClient.invalidateQueries('maps');
},
onError: (error) => {
console.error(error);
},
}
);
const handleRemoveLabel = (mapId: number, labelId: number) => {
removeLabelMultation.mutate({ mapId, labelId });
};
const isSelected = (id: number) => selected.indexOf(id) !== -1; const isSelected = (id: number) => selected.indexOf(id) !== -1;
return ( return (
<div className={classes.root}> <div className={classes.root}>
@ -419,7 +466,31 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
)} )}
{selected.length > 0 && <AddLabelButton />} {selected.length > 0 && (
<Tooltip
arrow={true}
title={intl.formatMessage({
id: 'map.tooltip-add',
defaultMessage: 'Add label to selected',
})}
>
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
style={{ marginLeft: '10px' }}
disableElevation={true}
startIcon={<LabelTwoTone />}
onClick={handleAddLabelClick}
>
<FormattedMessage
id="action.label"
defaultMessage="Add Label"
/>
</Button>
</Tooltip>
)}
</div> </div>
<div className={classes.toolbarListActions}> <div className={classes.toolbarListActions}>
@ -560,8 +631,13 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}>
<LabelsCell labels={row.labels} /> <LabelsCell
labels={row.labels}
onDelete={(lbl) => {
handleRemoveLabel(row.id, lbl.id);
}}
/>
</TableCell> </TableCell>
<TableCell className={classes.bodyCell}> <TableCell className={classes.bodyCell}>

View File

@ -0,0 +1,45 @@
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Typography from '@mui/material/Typography';
import BaseDialog from '../../action-dispatcher/base-dialog';
import { Label } from '../../../../classes/client';
export type LabelDeleteConfirmType = {
label: Label;
onClose: () => void;
onConfirm: () => void;
};
const LabelDeleteConfirm = ({ label, onClose, onConfirm }: LabelDeleteConfirmType): React.ReactElement => {
const intl = useIntl();
return (
<div>
<BaseDialog
onClose={onClose}
onSubmit={onConfirm}
title={intl.formatMessage({ id: 'label.delete-title', defaultMessage: 'Confirm label deletion' })}
submitButton={intl.formatMessage({
id: 'action.delete-title',
defaultMessage: 'Delete',
})}
>
<Alert severity="warning">
<AlertTitle>{intl.formatMessage({ id: 'label.delete-title', defaultMessage: 'Confirm label deletion' })}</AlertTitle>
<span>
<Typography fontWeight="bold" component="span">{label.title} </Typography>
<FormattedMessage
id="label.delete-description"
defaultMessage="will be deleted, including its associations to all existing maps. Do you want to continue?"
/>
</span>
</Alert>
</BaseDialog>
</div>
);
};
export default LabelDeleteConfirm;

View File

@ -1,58 +1,55 @@
import React from 'react'; import React from 'react';
import FormGroup from '@mui/material/FormGroup'; import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Divider from '@mui/material/Divider';
import AddIcon from '@mui/icons-material/Add';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import { Label as LabelComponent } from '../label'; import LabelComponent from '../label';
import Client, { Label, ErrorInfo } from '../../../../classes/client'; import Client, { Label, ErrorInfo, MapInfo } from '../../../../classes/client';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { activeInstance } from '../../../../redux/clientSlice'; import { activeInstance } from '../../../../redux/clientSlice';
import { StyledButton } from './styled'; import AddLabelForm from '../add-label-form';
import { LabelListContainer } from './styled';
export function LabelSelector(): React.ReactElement { export type LabelSelectorProps = {
const client: Client = useSelector(activeInstance); maps: MapInfo[];
onChange: (label: Label, checked: boolean) => void;
const { data: labels = [] } = useQuery<unknown, ErrorInfo, Label[]>('labels', async () => client.fetchLabels());
const [state, setState] = React.useState(labels.reduce((acc, label) => {
acc[label.id] = false //label.checked;
return acc;
}, {}),);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setState({ ...state, [event.target.id]: event.target.checked });
}; };
export function LabelSelector({ onChange, maps }: LabelSelectorProps): React.ReactElement {
const client: Client = useSelector(activeInstance);
const { data: labels = [] } = useQuery<unknown, ErrorInfo, Label[]>('labels', async () =>
client.fetchLabels()
);
const checkedLabelIds = labels
.map((l) => l.id)
.filter((labelId) => maps.every((m) => m.labels.find((l) => l.id === labelId)));
return ( return (
<Container> <Container>
<FormGroup> <FormGroup>
<AddLabelForm onAdd={(label) => onChange(label, true)} />
</FormGroup>
<LabelListContainer>
{labels.map(({ id, title, color }) => ( {labels.map(({ id, title, color }) => (
<FormControlLabel <FormControlLabel
key={id} key={id}
control={ control={
<Checkbox <Checkbox
id={`${id}`} id={`${id}`}
checked={state[id]} checked={checkedLabelIds.includes(id)}
onChange={handleChange} onChange={(e) => {
onChange({ id, title, color }, e.target.checked);
}}
name={title} name={title}
color="primary" color="primary"
/> />
} }
label={<LabelComponent name={title} color={color} />} label={<LabelComponent label={{ id, title, color }} size="big" />}
/> />
))} ))}
<Divider /> </LabelListContainer>
<StyledButton
color="primary"
startIcon={<AddIcon />}
>
{/* i18n */}
Add new label
</StyledButton>
</FormGroup>
</Container> </Container>
); );
} }

View File

@ -1,6 +1,8 @@
import FormGroup from '@mui/material/FormGroup';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from '@mui/material/Button';
export const StyledButton = styled(Button)` export const LabelListContainer = styled(FormGroup)`
margin: 4px; max-height: 400px;
flex-wrap: nowrap;
overflow-y: scroll;
`; `;

View File

@ -1,13 +1,38 @@
import React from 'react'; import React from 'react';
import { Color, StyledLabel, Name } from './styled'; import { LabelContainer, LabelText } from './styled';
type Props = { name: string, color: string }; import { Label } from '../../../../classes/client';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import DeleteIcon from '@mui/icons-material/Clear';
import IconButton from '@mui/material/IconButton';
type LabelSize = 'small' | 'big';
type LabelComponentProps = { label: Label; onDelete?: (label: Label) => void; size?: LabelSize };
export default function LabelComponent({ label, onDelete, size = 'small' }: LabelComponentProps): React.ReactElement<LabelComponentProps> {
const iconSize = size === 'small' ? {
height: '0.6em', width: '0.6em'
} : { height: '0.9em', width: '0.9em' };
export function Label({ name, color }: Props): React.ReactElement<Props> {
return ( return (
<StyledLabel> <LabelContainer color={label.color}>
<Color color={color} /> <LabelTwoTone htmlColor={label.color} style={iconSize} />
<Name>{name}</Name> <LabelText>{label.title}</LabelText>
</StyledLabel> {onDelete && (
<IconButton
color="default"
size="small"
aria-label="delete tag"
component="span"
onClick={(e) => {
e.stopPropagation();
onDelete(label);
}}
>
<DeleteIcon style={iconSize} />
</IconButton>
)}
</LabelContainer>
); );
} }

View File

@ -1,23 +1,15 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
const SIZE = 20; export const LabelContainer = styled.div`
display: inline-flex;
export const Color = styled.div`
width: ${SIZE}px;
height: ${SIZE}px;
border-radius: ${SIZE * 0.25}px;
border: 1px solid black;
margin: 1px ${SIZE * 0.5}px 1px 0px;
${props => props.color && css`
background-color: ${props.color};
`}
`;
export const StyledLabel = styled.div`
display: flex;
flex-direction: row; flex-direction: row;
margin: 4px;
padding: 4px;
align-items: center;
font-size: smaller;
`; `;
export const Name = styled.div` export const LabelText = styled.span`
flex: 1; margin-left: 4px;
margin-right: 2px;
`; `;

View File

@ -1,26 +1,35 @@
import React from 'react'; import React from 'react';
import Chip from '@mui/material/Chip'; import { LabelContainer, LabelText } from './styled';
import { Label } from '../../../../classes/client'; import { Label } from '../../../../classes/client';
import LabelTwoTone from '@mui/icons-material/LabelTwoTone'; import LabelTwoTone from '@mui/icons-material/LabelTwoTone';
import DeleteIcon from '@mui/icons-material/Clear';
import IconButton from '@mui/material/IconButton';
type Props = { type Props = {
labels: Label[], labels: Label[],
onDelete: (label: Label) => void,
}; };
export function LabelsCell({ labels }: Props): React.ReactElement<Props> { export function LabelsCell({ labels, onDelete }: Props): React.ReactElement<Props> {
return ( return (
<> <>
{labels.map(label => ( {labels.map(label => (
<Chip <LabelContainer
key={label.id} key={label.id}
size="small" color={label.color}
icon={<LabelTwoTone />} >
label={label.title} <LabelTwoTone htmlColor={label.color} style={{ height: '0.6em', width: '0.6em' }} />
clickable <LabelText>{ label.title }</LabelText>
color="primary" <IconButton color="default" size='small' aria-label="delete tag" component="span"
style={{ backgroundColor: label.color, opacity: '0.75' }} onClick={(e) => {
onDelete={() => { return 1; }} e.stopPropagation();
/> onDelete(label);
}}
>
<DeleteIcon style={{ height: '0.6em', width: '0.6em' }} />
</IconButton>
</LabelContainer>
))} ))}
</> </>
); );

View File

@ -0,0 +1,15 @@
import styled from 'styled-components';
export const LabelContainer = styled.div`
display: inline-flex;
flex-direction: row;
margin: 4px;
padding: 4px;
align-items: center;
font-size: smaller;
`;
export const LabelText = styled.span`
margin-left: 4px;
margin-right: 2px;
`;

View File

@ -33,6 +33,13 @@ export const useStyles = makeStyles((theme: Theme) =>
bodyCell: { bodyCell: {
border: '0px', border: '0px',
}, },
labelsCell: {
maxWidth: '300px',
overflow: 'hidden',
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},
visuallyHidden: { visuallyHidden: {
border: 0, border: 0,
clip: 'rect(0 0 0 0)', clip: 'rect(0 0 0 0)',

View File

@ -7,6 +7,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = merge(common, { module.exports = merge(common, {
mode: 'development', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
watch: true,
devServer: { devServer: {
contentBase: path.join(__dirname, 'dist'), contentBase: path.join(__dirname, 'dist'),
port: 3000, port: 3000,

View File

@ -5705,7 +5705,7 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dayjs@^1.10.4: dayjs@^1.10.4, dayjs@^1.10.7:
version "1.10.7" version "1.10.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==