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

View File

@ -21,6 +21,7 @@ import { $assert } from '@wisemapping/core-js';
import Point from '@wisemapping/web2d';
import { Mindmap } from '..';
import CommandContext from './CommandContext';
import ControlPoint from './ControlPoint';
import Events from './Events';
import NodeModel from './model/NodeModel';
import RelationshipModel from './model/RelationshipModel';
@ -44,7 +45,7 @@ abstract class ActionDispatcher extends Events {
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,9 +17,25 @@
*/
import { $assert, $defined } from '@wisemapping/core-js';
import DragTopic from './DragTopic';
import EventBusDispatcher from './layout/EventBusDispatcher';
import Workspace from './Workspace';
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._designerModel = workspace;
this._listeners = {};
@ -34,7 +50,7 @@ class DragManager {
const screen = workspace.getScreenManager();
const dragManager = this;
const me = this;
const mouseDownListener = function mouseDownListener(event) {
const mouseDownListener = function mouseDownListener() {
if (workspace.isWorkspaceEventsEnabled()) {
// Disable double drag...
workspace.enableWorkspaceEvents(false);
@ -62,11 +78,11 @@ class DragManager {
node.addEvent('mousedown', mouseDownListener);
}
remove(node) {
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 me = this;
const result = (event) => {
@ -98,7 +114,7 @@ class DragManager {
return result;
}
_buildMouseUpListener(workspace, node, dragNode, dragManager) {
protected _buildMouseUpListener(workspace: Workspace, node, dragNode, dragManager: DragManager) {
const screen = workspace.getScreenManager();
const me = this;
const result = (event) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import $ from 'jquery';
import { $assert, $defined } from '@wisemapping/core-js';
import {
Rect, Image, Line, Text, Group, ElementClass, Point
Rect, Image, Line, Text, Group, ElementClass, Point,
} from '@wisemapping/web2d';
import NodeGraph from './NodeGraph';
@ -35,7 +35,6 @@ import NoteEditor from './widget/NoteEditor';
import ActionDispatcher from './ActionDispatcher';
import LinkEditor from './widget/LinkEditor';
import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher';
import { TopicShape } from './model/INodeModel';
import NodeModel from './model/NodeModel';
@ -50,14 +49,25 @@ const ICON_SCALING_FACTOR = 1.3;
abstract class Topic extends NodeGraph {
private _innerShape: ElementClass;
private _relationships: Relationship[];
private _isInWorkspace: boolean;
// eslint-disable-next-line no-use-before-define
private _children: Topic[];
// eslint-disable-next-line no-use-before-define
private _parent: Topic | null;
private _outerShape: ElementClass;
private _text: Text | null;
private _iconsGroup: IconGroup;
private _connector: any;
private _connector: ShirinkConnector;
private _outgoingLine: Line;
constructor(model: NodeModel, options) {
@ -241,7 +251,7 @@ abstract class Topic extends NodeGraph {
result.setStroke(1, 'solid', stokeColor);
};
result.getSize = function getSize() { this.size };
result.getSize = function getSize() { return this.size; };
result.setPosition = () => {
// Overwrite behaviour ...
@ -1324,7 +1334,6 @@ abstract class Topic extends NodeGraph {
return result;
}
isChildTopic(childTopic: Topic): boolean {
let result = this.getId() === childTopic.getId();
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);
}
removeEvent(type: string, listener): void {
removeEvent(type: string, listener: (event: Event) => void): void {
$assert(type, 'type can not be null');
$assert(listener, 'listener can not be null');
this._workspace.removeEvent(type, listener);
@ -193,7 +193,7 @@ class Workspace {
const workspace = this._workspace;
const screenManager = this._screenManager;
const mWorkspace = this;
const mouseDownListener = function mouseDownListener(event) {
const mouseDownListener = function mouseDownListener(event: MouseEvent) {
if (!$defined(workspace._mouseMoveListener)) {
if (mWorkspace.isWorkspaceEventsEnabled()) {
mWorkspace.enableWorkspaceEvents(false);
@ -202,7 +202,7 @@ class Workspace {
const originalCoordOrigin = workspace.getCoordOrigin();
let wasDragged = false;
workspace._mouseMoveListener = (mouseMoveEvent) => {
workspace._mouseMoveListener = (mouseMoveEvent: MouseEvent) => {
const currentMousePosition = screenManager.getWorkspaceMousePosition(mouseMoveEvent);
const offsetX = currentMousePosition.x - mouseDownPosition.x;

View File

@ -18,6 +18,7 @@
import { $assert, $defined } from '@wisemapping/core-js';
import Command from '../Command';
import CommandContext from '../CommandContext';
import FeatureModel from '../model/FeatureModel';
import FeatureType from '../model/FeatureType';
class AddFeatureToTopicCommand extends Command {
@ -27,7 +28,7 @@ class AddFeatureToTopicCommand extends Command {
private _attributes: object;
private _featureModel: any;
private _featureModel: FeatureModel;
/*
* @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 _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(featureId), 'featureId can not be null');
$assert($defined(attributes), 'attributes can not be null');
@ -53,7 +53,7 @@ class ChangeFeatureToTopicCommand extends Command {
* Overrides abstract parent method
* @see {@link mindplot.Command.undoExecute}
*/
undoExecute(commandContext: any) {
undoExecute(commandContext: CommandContext) {
this.execute(commandContext);
}
}

View File

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

View File

@ -21,8 +21,6 @@ import CommandContext from '../CommandContext';
import Topic from '../Topic';
class GenericFunctionCommand extends Command {
private _discardDuplicated: string;
private _value: string | object | boolean | number;
private _topicsId: number[];
@ -42,7 +40,6 @@ class GenericFunctionCommand extends Command {
this._topicsId = topicsIds;
this._commandFunc = commandFunc;
this._oldValues = [];
this.discardDuplicated = undefined;
}
/**
@ -79,14 +76,6 @@ class GenericFunctionCommand extends Command {
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;

View File

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

View File

@ -18,13 +18,14 @@
import { $assert, $defined } from '@wisemapping/core-js';
import Command from '../Command';
import CommandContext from '../CommandContext';
import FeatureModel from '../model/FeatureModel';
class RemoveFeatureFromTopicCommand extends Command {
private _topicId: 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

View File

@ -16,9 +16,23 @@
* limitations under the License.
*/
import { $assert, $defined } from '@wisemapping/core-js';
import PositionType from '../PositionType';
import SizeType from '../SizeType';
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(size, 'size can not be null');
$assert(position, 'position can not be null');
@ -69,7 +83,7 @@ class Node {
}
/** */
setOrder(order) {
setOrder(order: number) {
$assert(
typeof order === 'number' && Number.isFinite(order),
`Order can not be null. Value:${order}`,
@ -148,7 +162,7 @@ class Node {
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.x), 'x can not be null');
$assert($defined(position.y), 'y can not be null');
@ -177,7 +191,7 @@ class Node {
) this._setProperty('position', position);
}
_setProperty(key, value) {
_setProperty(key: string, value) {
let prop = this._properties[key];
if (!prop) {
prop = {
@ -214,20 +228,13 @@ class Node {
/** @return {String} returns id, order, position, size and shrink information */
toString() {
return (
`[id:${
this.getId()
}, order:${
this.getOrder()
}, position: {${
this.getPosition().x
},${
this.getPosition().y
}}, size: {${
this.getSize().width
},${
this.getSize().height
}}, shrink:${
this.areChildrenShrunken()
`[id:${this.getId()
}, order:${this.getOrder()
}, position: {${this.getPosition().x
},${this.getPosition().y
}}, size: {${this.getSize().width
},${this.getSize().height
}}, shrink:${this.areChildrenShrunken()
}]`
);
}

View File

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

View File

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

View File

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

View File

@ -15,13 +15,13 @@ export class Locale {
}
}
export default class AppI18n {
public getUserLocale(): Locale {
export default abstract class AppI18n {
public static getUserLocale(): Locale {
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;
// Just remove the variant ...

View File

@ -90,10 +90,22 @@ class CacheDecoratorClient implements Client {
return this.client.fetchLabels();
}
createLabel(title: string, color: string): Promise<number> {
return this.client.createLabel(title, color);
}
deleteLabel(id: number): Promise<void> {
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> {
return this.client.fetchAccountInfo();
}

View File

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

View File

@ -11,7 +11,7 @@ import Client, {
ImportMapInfo,
Permission,
} from '..';
import { LocaleCode, localeFromStr, Locales } from '../../app-i18n';
import { LocaleCode, localeFromStr } from '../../app-i18n';
export default class RestClient implements Client {
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 : ''
}`,
model.content,
{ headers: { 'Content-Type': model.contentType } }
{ headers: { 'Content-Type': 'application/xml' } }
)
.then((response) => {
const mapId = response.headers.resourceid;
@ -214,7 +214,7 @@ export default class RestClient implements Client {
lastname: account.lastname ? account.lastname : '',
firstname: account.firstname ? account.firstname : '',
email: account.email,
locale: locale ? localeFromStr(locale) : Locales.EN,
locale: locale ? localeFromStr(locale) : undefined,
});
})
.catch((error) => {
@ -510,10 +510,59 @@ export default class RestClient implements Client {
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> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
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(() => {
success();
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,7 @@ const ImportDialog = ({ onClose }: CreateProps): React.ReactElement => {
description={intl.formatMessage({
id: 'import.description',
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' })}
>

View File

@ -12,6 +12,7 @@ import InfoDialog from './info-dialog';
import DeleteMultiselectDialog from './delete-multiselect-dialog';
import ExportDialog from './export-dialog';
import ShareDialog from './share-dialog';
import LabelDialog from './label-dialog';
export type BasicMapInfo = {
name: string;
@ -61,6 +62,7 @@ const ActionDispatcher = ({ mapsId, action, onClose, fromEditor }: ActionDialogP
<ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} />
)}
{action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />}
{action === 'label' && <LabelDialog onClose={handleOnClose} mapsId={mapsId} />}
</span>
);
};
@ -79,4 +81,9 @@ export type SimpleDialogProps = {
onClose: () => void;
};
export type MultiDialogProps = {
mapsId: number[];
onClose: () => void;
};
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 { useStyles } from './style';
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 { activeInstance } from '../../redux/clientSlice';
import { useSelector } from 'react-redux';
@ -40,6 +40,7 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import logoIcon from './logo-small.svg';
import poweredByIcon from './pwrdby-white.svg';
import LabelDeleteConfirm from './maps-list/label-delete-confirm';
export type Filter = GenericFilter | LabelFilter;
@ -64,10 +65,9 @@ const MapsPage = (): ReactElement => {
const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient();
const [activeDialog, setActiveDialog] = React.useState<ActionType | undefined>(undefined);
const [labelToDelete, setLabelToDelete] = React.useState<number | null>(null);
// Reload based on user preference ...
const appi18n = new AppI18n();
const userLocale = appi18n.getUserLocale();
const userLocale = AppI18n.getUserLocale();
const cache = createIntlCache();
const intl = createIntl({
@ -77,7 +77,6 @@ const MapsPage = (): ReactElement => {
}, cache)
useEffect(() => {
document.title = intl.formatMessage({
id: 'maps.page-title',
defaultMessage: 'My Maps | WiseMapping',
@ -85,7 +84,10 @@ const MapsPage = (): ReactElement => {
}, []);
const mutation = useMutation((id: number) => client.deleteLabel(id), {
onSuccess: () => queryClient.invalidateQueries('labels'),
onSuccess: () => {
queryClient.invalidateQueries('labels');
queryClient.invalidateQueries('maps');
},
onError: (error) => {
console.error(`Unexpected error ${error}`);
},
@ -238,7 +240,7 @@ const MapsPage = (): ReactElement => {
filter={buttonInfo.filter}
active={filter}
onClick={handleMenuClick}
onDelete={handleLabelDelete}
onDelete={setLabelToDelete}
key={`${buttonInfo.filter.type}:${buttonInfo.label}`}
/>
);
@ -259,6 +261,14 @@ const MapsPage = (): ReactElement => {
<MapsList filter={filter} />
</main>
</div>
{ labelToDelete && <LabelDeleteConfirm
onClose={() => setLabelToDelete(null)}
onConfirm={() => {
handleLabelDelete(labelToDelete);
setLabelToDelete(null);
}}
label={labels.find(l => l.id === labelToDelete)}
/> }
</IntlProvider>
);
};

View File

@ -3,9 +3,9 @@ import React from 'react';
import { useMutation, useQueryClient } from 'react-query';
import Client from '../../../classes/client';
import { useSelector } from 'react-redux';
import { activeInstance, fetchAccount } from '../../../redux/clientSlice';
import { activeInstance } from '../../../redux/clientSlice';
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 Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
@ -49,7 +49,7 @@ const LanguageMenu = (): React.ReactElement => {
mutation.mutate(localeCode);
};
const accountInfo = fetchAccount();
const userLocale = AppI18n.getUserLocale();
return (
<span>
<Tooltip
@ -68,7 +68,7 @@ const LanguageMenu = (): React.ReactElement => {
onClick={handleMenu}
startIcon={<TranslateTwoTone style={{ color: 'inherit' }} />}
>
{accountInfo?.locale?.label}
{userLocale.label}
</Button>
</Tooltip>
<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 { useSelector } from 'react-redux';
import { activeInstance, fetchAccount } from '../../../redux/clientSlice';
import { activeInstance } from '../../../redux/clientSlice';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import Client, { ErrorInfo, Label, MapInfo } from '../../../classes/client';
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 SearchIcon from '@mui/icons-material/Search';
import { AddLabelButton } from './add-label-button';
import relativeTime from 'dayjs/plugin/relativeTime';
import { LabelsCell } from './labels-cell';
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);
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 => {
const classes = useStyles();
const [order, setOrder] = React.useState<Order>('desc');
@ -251,10 +270,8 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
const queryClient = useQueryClient();
// Configure locale ...
const account = fetchAccount();
if (account) {
dayjs.locale(account.locale.code);
}
const userLocale = AppI18n.getUserLocale();
dayjs.locale(userLocale.code);
useEffect(() => {
setSelected([]);
@ -331,7 +348,7 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
event.stopPropagation();
};
};
9;
const starredMultation = useMutation<void, ErrorInfo, number>(
(id: number) => {
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;
return (
<div className={classes.root}>
@ -419,7 +466,31 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</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 className={classes.toolbarListActions}>
@ -560,8 +631,13 @@ export const MapsList = (props: MapsListProps): React.ReactElement => {
</Tooltip>
</TableCell>
<TableCell className={classes.bodyCell}>
<LabelsCell labels={row.labels} />
<TableCell className={[classes.bodyCell, classes.labelsCell].join(' ')}>
<LabelsCell
labels={row.labels}
onDelete={(lbl) => {
handleRemoveLabel(row.id, lbl.id);
}}
/>
</TableCell>
<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 FormGroup from '@mui/material/FormGroup';
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 Container from '@mui/material/Container';
import { Label as LabelComponent } from '../label';
import Client, { Label, ErrorInfo } from '../../../../classes/client';
import LabelComponent from '../label';
import Client, { Label, ErrorInfo, MapInfo } from '../../../../classes/client';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { activeInstance } from '../../../../redux/clientSlice';
import { StyledButton } from './styled';
import AddLabelForm from '../add-label-form';
import { LabelListContainer } from './styled';
export function LabelSelector(): React.ReactElement {
const client: Client = useSelector(activeInstance);
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 type LabelSelectorProps = {
maps: MapInfo[];
onChange: (label: Label, checked: boolean) => void;
};
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 (
<Container>
<FormGroup>
<AddLabelForm onAdd={(label) => onChange(label, true)} />
</FormGroup>
<LabelListContainer>
{labels.map(({ id, title, color }) => (
<FormControlLabel
key={id}
control={
<Checkbox
id={`${id}`}
checked={state[id]}
onChange={handleChange}
checked={checkedLabelIds.includes(id)}
onChange={(e) => {
onChange({ id, title, color }, e.target.checked);
}}
name={title}
color="primary"
/>
}
label={<LabelComponent name={title} color={color} />}
label={<LabelComponent label={{ id, title, color }} size="big" />}
/>
))}
<Divider />
<StyledButton
color="primary"
startIcon={<AddIcon />}
>
{/* i18n */}
Add new label
</StyledButton>
</FormGroup>
</LabelListContainer>
</Container>
);
}

View File

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

View File

@ -1,13 +1,38 @@
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 (
<StyledLabel>
<Color color={color} />
<Name>{name}</Name>
</StyledLabel>
<LabelContainer color={label.color}>
<LabelTwoTone htmlColor={label.color} style={iconSize} />
<LabelText>{label.title}</LabelText>
{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 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;
export const LabelContainer = styled.div`
display: inline-flex;
flex-direction: row;
margin: 4px;
padding: 4px;
align-items: center;
font-size: smaller;
`;
export const Name = styled.div`
flex: 1;
export const LabelText = styled.span`
margin-left: 4px;
margin-right: 2px;
`;

View File

@ -1,26 +1,35 @@
import React from 'react';
import Chip from '@mui/material/Chip';
import { LabelContainer, LabelText } from './styled';
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 Props = {
labels: Label[],
onDelete: (label: Label) => void,
};
export function LabelsCell({ labels }: Props): React.ReactElement<Props> {
export function LabelsCell({ labels, onDelete }: Props): React.ReactElement<Props> {
return (
<>
{labels.map(label => (
<Chip
<LabelContainer
key={label.id}
size="small"
icon={<LabelTwoTone />}
label={label.title}
clickable
color="primary"
style={{ backgroundColor: label.color, opacity: '0.75' }}
onDelete={() => { return 1; }}
/>
color={label.color}
>
<LabelTwoTone htmlColor={label.color} style={{ height: '0.6em', width: '0.6em' }} />
<LabelText>{ label.title }</LabelText>
<IconButton color="default" size='small' aria-label="delete tag" component="span"
onClick={(e) => {
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: {
border: '0px',
},
labelsCell: {
maxWidth: '300px',
overflow: 'hidden',
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},
visuallyHidden: {
border: 0,
clip: 'rect(0 0 0 0)',

View File

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

View File

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