mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-15 19:47:57 +01:00
Merge branch 'develop'
This commit is contained in:
commit
e1f71612ba
@ -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}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ class CentralTopic extends Topic {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
workoutIncomingConnectionPoint(): Point {
|
workoutIncomingConnectionPoint(): Point {
|
||||||
return this.getPosition();
|
return this.getPosition();
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,18 @@ 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;
|
||||||
|
|
||||||
abstract undoExecute(commandContext:CommandContext):void;
|
abstract undoExecute(commandContext: CommandContext): void;
|
||||||
|
|
||||||
getId():number {
|
getId(): number {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
|
@ -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;
|
@ -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 {
|
||||||
|
@ -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]) {
|
||||||
|
@ -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);
|
@ -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) => {
|
@ -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();
|
||||||
|
@ -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') {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -43,7 +44,7 @@ class RemoveFeatureFromTopicCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Overrides abstract parent method
|
* Overrides abstract parent method
|
||||||
*/
|
*/
|
||||||
execute(commandContext:CommandContext):void {
|
execute(commandContext: CommandContext): void {
|
||||||
const topic = commandContext.findTopics([this._topicId])[0];
|
const topic = commandContext.findTopics([this._topicId])[0];
|
||||||
const feature = topic.findFeatureById(this._featureId);
|
const feature = topic.findFeatureById(this._featureId);
|
||||||
topic.removeFeature(feature);
|
topic.removeFeature(feature);
|
||||||
@ -54,7 +55,7 @@ class RemoveFeatureFromTopicCommand extends Command {
|
|||||||
* Overrides abstract parent method
|
* Overrides abstract parent method
|
||||||
* @see {@link mindplot.Command.undoExecute}
|
* @see {@link mindplot.Command.undoExecute}
|
||||||
*/
|
*/
|
||||||
undoExecute(commandContext:CommandContext) {
|
undoExecute(commandContext: CommandContext) {
|
||||||
const topic = commandContext.findTopics([this._topicId])[0];
|
const topic = commandContext.findTopics([this._topicId])[0];
|
||||||
topic.addFeature(this._oldFeature);
|
topic.addFeature(this._oldFeature);
|
||||||
this._oldFeature = null;
|
this._oldFeature = null;
|
||||||
|
@ -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');
|
||||||
@ -172,12 +186,12 @@ class Node {
|
|||||||
const currentPos = this.getPosition();
|
const currentPos = this.getPosition();
|
||||||
if (
|
if (
|
||||||
currentPos == null
|
currentPos == null
|
||||||
|| Math.abs(currentPos.x - position.x) > 2
|
|| Math.abs(currentPos.x - position.x) > 2
|
||||||
|| Math.abs(currentPos.y - position.y) > 2
|
|| Math.abs(currentPos.y - position.y) > 2
|
||||||
) 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()
|
|
||||||
}]`
|
}]`
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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 ...
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
})
|
})
|
||||||
|
@ -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} />
|
||||||
|
@ -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 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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' })}
|
||||||
>
|
>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
@ -0,0 +1,10 @@
|
|||||||
|
import createStyles from '@mui/styles/createStyles';
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
paper: {
|
||||||
|
maxWidth: '420px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
`;
|
@ -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}>
|
||||||
|
@ -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;
|
@ -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());
|
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 [state, setState] = React.useState(labels.reduce((acc, label) => {
|
const checkedLabelIds = labels
|
||||||
acc[label.id] = false //label.checked;
|
.map((l) => l.id)
|
||||||
return acc;
|
.filter((labelId) => maps.every((m) => m.labels.find((l) => l.id === labelId)));
|
||||||
}, {}),);
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
return (
|
||||||
setState({ ...state, [event.target.id]: event.target.checked });
|
<Container>
|
||||||
};
|
<FormGroup>
|
||||||
|
<AddLabelForm onAdd={(label) => onChange(label, true)} />
|
||||||
return (
|
</FormGroup>
|
||||||
<Container>
|
<LabelListContainer>
|
||||||
<FormGroup>
|
{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={checkedLabelIds.includes(id)}
|
||||||
checked={state[id]}
|
onChange={(e) => {
|
||||||
onChange={handleChange}
|
onChange({ id, title, color }, e.target.checked);
|
||||||
name={title}
|
}}
|
||||||
color="primary"
|
name={title}
|
||||||
/>
|
color="primary"
|
||||||
}
|
/>
|
||||||
label={<LabelComponent name={title} color={color} />}
|
}
|
||||||
/>
|
label={<LabelComponent label={{ id, title, color }} size="big" />}
|
||||||
))}
|
/>
|
||||||
<Divider />
|
))}
|
||||||
<StyledButton
|
</LabelListContainer>
|
||||||
color="primary"
|
</Container>
|
||||||
startIcon={<AddIcon />}
|
);
|
||||||
>
|
|
||||||
{/* i18n */}
|
|
||||||
Add new label
|
|
||||||
</StyledButton>
|
|
||||||
</FormGroup>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
`;
|
@ -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';
|
||||||
|
|
||||||
export function Label({ name, color }: Props): React.ReactElement<Props> {
|
|
||||||
return (
|
type LabelSize = 'small' | 'big';
|
||||||
<StyledLabel>
|
type LabelComponentProps = { label: Label; onDelete?: (label: Label) => void; size?: LabelSize };
|
||||||
<Color color={color} />
|
|
||||||
<Name>{name}</Name>
|
export default function LabelComponent({ label, onDelete, size = 'small' }: LabelComponentProps): React.ReactElement<LabelComponentProps> {
|
||||||
</StyledLabel>
|
const iconSize = size === 'small' ? {
|
||||||
);
|
height: '0.6em', width: '0.6em'
|
||||||
|
} : { height: '0.9em', width: '0.9em' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
`;
|
@ -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>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
`;
|
@ -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)',
|
||||||
|
@ -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,
|
||||||
|
@ -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==
|
||||||
|
Loading…
Reference in New Issue
Block a user