Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

optimized copy/paste/cut in groups #285

Merged
merged 5 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ E.g., if it was used in a menu and the menu is red, the circle would be red.
### **WORK IN PROGRESS**
-->
## Changelog
### **WORK IN PROGRESS**
* (foxriver76) optimized copy/paste/cut in groups

### 2.9.11 (2024-01-02)
* (foxriver76) fixed bug with visibility calculation

Expand Down
75 changes: 49 additions & 26 deletions src/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from '@iobroker/adapter-react-v5';
import { recalculateFields, store, updateProject } from './Store';
import {
isGroup, getNewWidgetId, getNewGroupId, pasteGroup, unsyncMultipleWidgets, deepClone,
isGroup, getNewWidgetId, getNewGroupId, pasteGroup, unsyncMultipleWidgets, deepClone, pasteSingleWidget,
} from './Utils/utils';

import Attributes from './Attributes';
Expand Down Expand Up @@ -528,7 +528,7 @@ class App extends Runtime {
const widgets = {};
const groupMembers = {};
const project = deepClone(store.getState().visProject);
const { visProject } = store.getState();
const { visProject } = deepClone(store.getState());

this.state.selectedWidgets.forEach(selectedWidget => {
widgets[selectedWidget] = visProject[this.state.selectedView].widgets[selectedWidget];
Expand All @@ -540,8 +540,28 @@ class App extends Runtime {
}
}

if (type === 'cut' && project[this.state.selectedView]) {
delete project[this.state.selectedView].widgets[selectedWidget];
if (project[this.state.selectedView]) {
const widget = project[this.state.selectedView].widgets[selectedWidget];

if (widget.groupid) {
delete widgets[selectedWidget].groupid;
delete widgets[selectedWidget].grouped;
delete widget._id;
}

if (type === 'cut') {
if (widget.groupid) {
const group = project[this.state.selectedView].widgets[widget.groupid];

const pos = group.data.members.indexOf(selectedWidget);

if (pos !== -1) {
group.data.members.splice(pos, 1);
}
}

delete project[this.state.selectedView].widgets[selectedWidget];
}
}
});

Expand All @@ -562,15 +582,15 @@ class App extends Runtime {
};

pasteWidgets = async () => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widgets = project[this.state.selectedView].widgets;

const newKeys = [];
let widgetOffset = 0;
let groupOffset = 0;

for (const clipboardWidgetId of Object.keys(this.state.widgetsClipboard.widgets)) {
const newWidget = JSON.parse(JSON.stringify(this.state.widgetsClipboard.widgets[clipboardWidgetId]));
const newWidget = deepClone(this.state.widgetsClipboard.widgets[clipboardWidgetId]);
if (this.state.widgetsClipboard.type === 'copy' && this.state.selectedView === this.state.widgetsClipboard.view) {
const boundingRect = App.getWidgetRelativeRect(clipboardWidgetId);
newWidget.style = this.pxToPercent(newWidget.style, {
Expand All @@ -581,17 +601,18 @@ class App extends Runtime {
let newKey;

if (isGroup(newWidget)) {
pasteGroup({
newKey = pasteGroup({
group: newWidget, widgets, offset: groupOffset, groupMembers: this.state.widgetsClipboard.groupMembers, project: store.getState().visProject,
});
newKey = getNewGroupId(store.getState().visProject, groupOffset);

groupOffset++;
} else {
newKey = getNewWidgetId(store.getState().visProject, widgetOffset);
newKey = pasteSingleWidget({
widget: newWidget, offset: widgetOffset, project: store.getState().visProject, selectedGroup: this.state.selectedGroup, widgets,
});
widgetOffset++;
}

widgets[newKey] = newWidget;
newKeys.push(newKey);
}

Expand All @@ -601,12 +622,12 @@ class App extends Runtime {
};

cloneWidgets = async () => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widgets = project[this.state.selectedView].widgets;

const newKeys = [];
this.state.selectedWidgets.forEach(selectedWidget => {
const newWidget = JSON.parse(JSON.stringify(widgets[selectedWidget]));
const newWidget = deepClone(widgets[selectedWidget]);
const boundingRect = App.getWidgetRelativeRect(selectedWidget);
newWidget.style = this.pxToPercent(newWidget.style, {
left: boundingRect.left + 10,
Expand All @@ -618,8 +639,10 @@ class App extends Runtime {
group: newWidget, widgets, groupMembers: widgets, project: store.getState().visProject,
});
} else {
const newKey = getNewWidgetId(store.getState().visProject);
widgets[newKey] = newWidget;
const newKey = pasteSingleWidget({
widget: newWidget, project: store.getState().visProject, selectedGroup: this.state.selectedGroup, widgets,
});

newKeys.push(newKey);
}
});
Expand All @@ -629,7 +652,7 @@ class App extends Runtime {
};

alignWidgets = type => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widgets = project[this.state.selectedView].widgets;
const newCoordinates = {
left: 0, top: 0, width: 0, height: 0, right: 0, bottom: 0,
Expand Down Expand Up @@ -790,7 +813,7 @@ class App extends Runtime {
};

orderWidgets = type => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widgets = project[this.state.selectedView].widgets;
let minZ = 0;
let maxZ = 0;
Expand Down Expand Up @@ -951,7 +974,7 @@ class App extends Runtime {
this.historyTimer = setTimeout(() => {
this.historyTimer = null;

let history = JSON.parse(JSON.stringify(this.state.history));
let history = deepClone(this.state.history);
let historyCursor = this.state.historyCursor;
if (historyCursor !== history.length - 1) {
history = history.slice(0, historyCursor + 1);
Expand Down Expand Up @@ -1090,7 +1113,7 @@ class App extends Runtime {
};

onWidgetsChanged = (changedData, view, viewSettings) => {
this.tempProject = this.tempProject || JSON.parse(JSON.stringify(store.getState().visProject));
this.tempProject = this.tempProject || deepClone(store.getState().visProject);
changedData && changedData.forEach(item => {
if (item.style) {
const currentStyle = this.tempProject[item.view].widgets[item.wid].style;
Expand Down Expand Up @@ -1216,7 +1239,7 @@ class App extends Runtime {

installWidget = async (widgetId, id) => {
if (window.VisMarketplace?.api) {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const marketplaceWidget = await window.VisMarketplace.api.apiGetWidgetRevision(widgetId, id);
if (!project.___settings.marketplace) {
project.___settings.marketplace = [];
Expand All @@ -1232,7 +1255,7 @@ class App extends Runtime {
};

uninstallWidget = async widget => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widgetIndex = project.___settings.marketplace.findIndex(item => item.id === widget);
if (widgetIndex !== -1) {
project.___settings.marketplace.splice(widgetIndex, 1);
Expand All @@ -1245,7 +1268,7 @@ class App extends Runtime {

widgets.forEach(_widget => {
if (_widget.isRoot) {
_widget.marketplace = JSON.parse(JSON.stringify(store.getState().visProject.___settings.marketplace.find(item => item.id === id)));
_widget.marketplace = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.id === id));
}
if (isGroup(_widget)) {
let newKey = getNewGroupId(store.getState().visProject);
Expand Down Expand Up @@ -1289,17 +1312,17 @@ class App extends Runtime {
};

addMarketplaceWidget = async (id, x, y, widgetId, oldData, oldStyle) => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const widgets = JSON.parse(JSON.stringify(store.getState().visProject.___settings.marketplace.find(item => item.id === id).widget));
const project = deepClone(store.getState().visProject);
const widgets = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.id === id).widget);
this.importMarketplaceWidget(project, this.state.selectedView, widgets, id, x, y, widgetId, oldData, oldStyle);
await this.changeProject(project);
};

updateWidget = async id => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
const widget = project[this.state.selectedView].widgets[id];
if (widget && widget.marketplace) {
const marketplace = JSON.parse(JSON.stringify(store.getState().visProject.___settings.marketplace.find(item => item.widget_id === widget.marketplace.widget_id)));
const marketplace = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.widget_id === widget.marketplace.widget_id));
await this.deleteWidgetsAction();
await this.addMarketplaceWidget(marketplace.id, null, null, id, widget.data, widget.style);
}
Expand Down Expand Up @@ -1450,7 +1473,7 @@ class App extends Runtime {
<IconButton
size="small"
onClick={() => {
const project = JSON.parse(JSON.stringify(store.getState().visProject));
const project = deepClone(store.getState().visProject);
project.___settings.openedViews = [this.state.selectedView];
this.changeProject(project, true);
}}
Expand Down
7 changes: 4 additions & 3 deletions src/src/Toolbar/WidgetExportDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { Utils, I18n } from '@iobroker/adapter-react-v5';

import IODialog from '../Components/IODialog';
import CustomAceEditor from '../Components/CustomAceEditor';
import { deepClone } from '../Utils/utils';

const WidgetExportDialog = props => {
const widgets = props.selectedWidgets.map(wid => {
const w = JSON.parse(JSON.stringify(props.widgets[wid]));
const w = deepClone(props.widgets[wid]);
w._id = wid;
return w;
});
Expand All @@ -25,13 +26,13 @@ const WidgetExportDialog = props => {
const newId = `f${gIdx.toString().padStart(6, '0')}`;
gIdx++;

if (widget.data && widget.data.members) {
if (widget.data?.members) {
const members = [];
widget.data.members.forEach(member => {
if (groupWidgets.includes(member)) {
return;
}
const memberWidget = JSON.parse(JSON.stringify(props.widgets[member]));
const memberWidget = deepClone(props.widgets[member]);
memberWidget._id = `i${wIdx.toString().padStart(6, '0')}`;
wIdx++;
members.push(memberWidget._id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
import { useRef, useState } from 'react';
import React, { useRef, useState } from 'react';

import {
Button, Dialog, DialogActions, DialogContent, DialogTitle,
Expand All @@ -8,13 +7,24 @@ import {
import { Close as CloseIcon, ImportExport } from '@mui/icons-material';

import { I18n } from '@iobroker/adapter-react-v5';
import { isGroup, getNewGroupId, getNewWidgetId } from '../Utils/utils';
import {
isGroup, getNewGroupId, getNewWidgetId, deepClone,
} from '@/Utils/utils';

import { useFocus } from '../Utils';
import { useFocus } from '@/Utils';
import { store } from '@/Store';
import { GroupWidget, Project, Widget } from '@/types';
import CustomAceEditor from '../Components/CustomAceEditor';
import { store } from '../Store';

const WidgetImportDialog = props => {
interface WidgetImportDialogProps {
changeProject: (project: Project) => void;
onClose:() => void;
themeType: string;
selectedView: string;
selectedGroup: string;
}

const WidgetImportDialog = (props: WidgetImportDialogProps) => {
const [data, setData] = useState('');
const [error, setError] = useState(false);

Expand All @@ -24,9 +34,9 @@ const WidgetImportDialog = props => {

const importWidgets = () => {
const { visProject } = store.getState();
const project = JSON.parse(JSON.stringify(visProject));
const widgets = JSON.parse(data);
const newWidgets = {};
const project = deepClone(visProject);
const widgets: Widget[] = JSON.parse(data);
const newWidgets: Record<string, Widget> = {};
let groupOffset = 0;
let widgetOffset = 0;

Expand All @@ -45,17 +55,25 @@ const WidgetImportDialog = props => {
} else {
const newKey = getNewWidgetId(visProject, widgetOffset++);
newWidgets[newKey] = widget;
if (widget.grouped && newWidgets[widget.groupid] && newWidgets[widget.groupid].data && newWidgets[widget.groupid].data.members) {
if (widget.grouped && widget.groupid && newWidgets[widget.groupid]?.data?.members) {
// find group
const pos = newWidgets[widget.groupid].data.members.indexOf(widget._id);
const pos = (newWidgets[widget.groupid] as GroupWidget).data.members.indexOf(widget._id as string);
if (pos !== -1) {
newWidgets[widget.groupid].data.members[pos] = newKey;
(newWidgets[widget.groupid] as GroupWidget).data.members[pos] = newKey;
}
}
}
}

Object.keys(newWidgets).forEach(wid => delete newWidgets[wid]._id);
Object.keys(newWidgets).forEach(wid => {
delete newWidgets[wid]._id;

if (!isGroup(newWidgets[wid]) && props.selectedGroup !== undefined) {
newWidgets[wid].grouped = true;
newWidgets[wid].groupid = props.selectedGroup;
(project[props.selectedView].widgets[props.selectedGroup] as GroupWidget).data.members.push(wid);
}
});

project[props.selectedView].widgets = { ...project[props.selectedView].widgets, ...newWidgets };

Expand All @@ -69,20 +87,7 @@ const WidgetImportDialog = props => {
maxWidth="lg"
>
<DialogTitle>{I18n.t('Import widgets')}</DialogTitle>
<DialogContent
onKeyUp={e => {
if (props.action) {
if (!props.actionDisabled && !props.keyboardDisabled) {
if (e.keyCode === 13) {
props.action();
if (!props.actionNoClose) {
props.onClose();
}
}
}
}
}}
>
<DialogContent>
<CustomAceEditor
type="json"
error={error}
Expand Down Expand Up @@ -119,6 +124,7 @@ const WidgetImportDialog = props => {
</Button>
<Button
variant="contained"
// @ts-expect-error works like that
color="grey"
onClick={props.onClose}
startIcon={<CloseIcon />}
Expand All @@ -128,11 +134,4 @@ const WidgetImportDialog = props => {
</DialogActions>
</Dialog>;
};

WidgetImportDialog.propTypes = {
changeProject: PropTypes.func,
onClose: PropTypes.func,
themeType: PropTypes.string,
selectedView: PropTypes.string,
};
export default WidgetImportDialog;
3 changes: 2 additions & 1 deletion src/src/Toolbar/Widgets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ const Widgets = props => {
Icon: BiImport,
name: 'Import widgets',
size: 'normal',
disabled: !props.editMode || !!props.selectedGroup,
disabled: !props.editMode,
onClick: () => setImportDialog(true),
}],
[{
Expand Down Expand Up @@ -381,6 +381,7 @@ const Widgets = props => {
changeProject={props.changeProject}
selectedView={props.selectedView}
project={store.getState().visProject}
selectedGroup={props.selectedGroup}
themeType={props.themeType}
/> : null}
{exportDialog ? <WidgetExportDialog
Expand Down
Loading
Loading