mirror of
https://github.com/muerwre/orchidmap-front.git
synced 2025-04-25 02:56:41 +07:00
setMode --> changeMode
This commit is contained in:
parent
5e55434772
commit
d0f419c18b
9 changed files with 80 additions and 42 deletions
|
@ -2,12 +2,12 @@ import React from 'react';
|
||||||
|
|
||||||
import { MODES } from '~/constants/modes';
|
import { MODES } from '~/constants/modes';
|
||||||
import { Icon } from '~/components/panels/Icon';
|
import { Icon } from '~/components/panels/Icon';
|
||||||
import { editorSetMode, editorStopEditing } from '~/redux/editor/actions';
|
import { editorChangeMode, editorStopEditing } from '~/redux/editor/actions';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
editorStopEditing,
|
editorStopEditing,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class CancelDialogUnconnected extends React.Component<Props, void> {
|
||||||
};
|
};
|
||||||
|
|
||||||
proceed = () => {
|
proceed = () => {
|
||||||
this.props.editorSetMode(MODES.NONE);
|
this.props.editorChangeMode(MODES.NONE);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -20,7 +20,7 @@ const mapStateToProps = state => ({
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
editorCancelSave: EDITOR_ACTIONS.editorCancelSave,
|
editorCancelSave: EDITOR_ACTIONS.editorCancelSave,
|
||||||
editorSetMode: EDITOR_ACTIONS.editorSetMode,
|
editorChangeMode: EDITOR_ACTIONS.editorChangeMode,
|
||||||
editorSendSaveRequest: EDITOR_ACTIONS.editorSendSaveRequest,
|
editorSendSaveRequest: EDITOR_ACTIONS.editorSendSaveRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class SaveDialogUnconnected extends React.Component<Props, State> {
|
||||||
|
|
||||||
forceSaveRequest = e => this.editorSendSaveRequest(e, true);
|
forceSaveRequest = e => this.editorSendSaveRequest(e, true);
|
||||||
|
|
||||||
cancelSaving = () => this.props.editorSetMode(MODES.NONE);
|
cancelSaving = () => this.props.editorChangeMode(MODES.NONE);
|
||||||
|
|
||||||
onCopy = e => {
|
onCopy = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Icon } from '~/components/panels/Icon';
|
||||||
import { EditorDialog } from '~/components/panels/EditorDialog';
|
import { EditorDialog } from '~/components/panels/EditorDialog';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
editorStartEditing,
|
editorStartEditing,
|
||||||
editorStopEditing,
|
editorStopEditing,
|
||||||
editorTakeAShot,
|
editorTakeAShot,
|
||||||
|
@ -21,7 +21,7 @@ const mapStateToProps = (state: IState) =>
|
||||||
pick(['mode', 'changed', 'editing', 'features'], selectEditor(state));
|
pick(['mode', 'changed', 'editing', 'features'], selectEditor(state));
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
editorStartEditing,
|
editorStartEditing,
|
||||||
editorStopEditing,
|
editorStopEditing,
|
||||||
editorTakeAShot,
|
editorTakeAShot,
|
||||||
|
@ -54,12 +54,12 @@ class EditorPanelUnconnected extends PureComponent<Props, void> {
|
||||||
this.props.editorKeyPressed(event);
|
this.props.editorKeyPressed(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
startPolyMode = () => this.props.editorSetMode(MODES.POLY);
|
startPolyMode = () => this.props.editorChangeMode(MODES.POLY);
|
||||||
startStickerMode = () => this.props.editorSetMode(MODES.STICKERS_SELECT);
|
startStickerMode = () => this.props.editorChangeMode(MODES.STICKERS_SELECT);
|
||||||
startRouterMode = () => this.props.editorSetMode(MODES.ROUTER);
|
startRouterMode = () => this.props.editorChangeMode(MODES.ROUTER);
|
||||||
startTrashMode = () => this.props.editorSetMode(MODES.TRASH);
|
startTrashMode = () => this.props.editorChangeMode(MODES.TRASH);
|
||||||
startSaveMode = () => {
|
startSaveMode = () => {
|
||||||
this.props.editorSetMode(MODES.SAVE);
|
this.props.editorChangeMode(MODES.SAVE);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -19,7 +19,7 @@ const mapStateToProps = (state: IState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
editorSetMode: EDITOR_ACTIONS.editorSetMode,
|
editorChangeMode: EDITOR_ACTIONS.editorChangeMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
@ -29,11 +29,11 @@ const TopRightPanelUnconnected = ({
|
||||||
logo,
|
logo,
|
||||||
markers_shown,
|
markers_shown,
|
||||||
editing,
|
editing,
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const startProviderMode = useCallback(() => editorSetMode(MODES.PROVIDER), [editorSetMode]);
|
const startProviderMode = useCallback(() => editorChangeMode(MODES.PROVIDER), [editorChangeMode]);
|
||||||
const startLogoMode = useCallback(() => editorSetMode(MODES.LOGO), [editorSetMode]);
|
const startLogoMode = useCallback(() => editorChangeMode(MODES.LOGO), [editorChangeMode]);
|
||||||
const clearMode = useCallback(() => editorSetMode(MODES.NONE), [editorSetMode]);
|
const clearMode = useCallback(() => editorChangeMode(MODES.NONE), [editorChangeMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="status-panel top right">
|
<div className="status-panel top right">
|
||||||
|
|
|
@ -7,10 +7,17 @@ export const editorSetEditing = (editing: IEditorState['editing']) => ({
|
||||||
type: EDITOR_ACTIONS.SET_EDITING,
|
type: EDITOR_ACTIONS.SET_EDITING,
|
||||||
editing,
|
editing,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const editorChangeMode = (mode: IEditorState['mode']) => ({
|
||||||
|
type: EDITOR_ACTIONS.CHANGE_MODE,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
export const editorSetMode = (mode: IEditorState['mode']) => ({
|
export const editorSetMode = (mode: IEditorState['mode']) => ({
|
||||||
type: EDITOR_ACTIONS.SET_MODE,
|
type: EDITOR_ACTIONS.SET_MODE,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const editorSetDistance = (distance: IEditorState['distance']) => ({
|
export const editorSetDistance = (distance: IEditorState['distance']) => ({
|
||||||
type: EDITOR_ACTIONS.SET_DISTANCE,
|
type: EDITOR_ACTIONS.SET_DISTANCE,
|
||||||
distance,
|
distance,
|
||||||
|
|
|
@ -2,6 +2,7 @@ const P = 'EDITOR';
|
||||||
|
|
||||||
export const EDITOR_ACTIONS = {
|
export const EDITOR_ACTIONS = {
|
||||||
SET_EDITING: `${P}-SET_EDITING`,
|
SET_EDITING: `${P}-SET_EDITING`,
|
||||||
|
CHANGE_MODE: `${P}-CHANGE_MODE`,
|
||||||
SET_MODE: `${P}-SET_MODE`,
|
SET_MODE: `${P}-SET_MODE`,
|
||||||
SET_DISTANCE: `${P}-SET_DISTANCE`,
|
SET_DISTANCE: `${P}-SET_DISTANCE`,
|
||||||
SET_CHANGED: `${P}-SET_CHANGED`,
|
SET_CHANGED: `${P}-SET_CHANGED`,
|
||||||
|
|
|
@ -20,7 +20,7 @@ const setChanged = (
|
||||||
changed,
|
changed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setMode = (state, { mode }: ReturnType<typeof ACTIONS.editorSetMode>): IEditorState => ({
|
const setMode = (state, { mode }: ReturnType<typeof ACTIONS.editorChangeMode>): IEditorState => ({
|
||||||
...state,
|
...state,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { simplify } from '~/utils/simplify';
|
||||||
import {
|
import {
|
||||||
editorHideRenderer,
|
editorHideRenderer,
|
||||||
editorSetChanged,
|
editorSetChanged,
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
editorSetReady,
|
editorSetReady,
|
||||||
editorSetRenderer,
|
editorSetRenderer,
|
||||||
editorSetDialogActive,
|
editorSetDialogActive,
|
||||||
|
@ -25,6 +25,8 @@ import {
|
||||||
editorSearchNominatim,
|
editorSearchNominatim,
|
||||||
editorSetDialog,
|
editorSetDialog,
|
||||||
editorSetNominatim,
|
editorSetNominatim,
|
||||||
|
editorSetMode,
|
||||||
|
editorSetRouter,
|
||||||
} from '~/redux/editor/actions';
|
} from '~/redux/editor/actions';
|
||||||
import { getUrlData, pushPath } from '~/utils/history';
|
import { getUrlData, pushPath } from '~/utils/history';
|
||||||
import { MODES } from '~/constants/modes';
|
import { MODES } from '~/constants/modes';
|
||||||
|
@ -70,11 +72,11 @@ function* stopEditingSaga() {
|
||||||
const { path } = getUrlData();
|
const { path } = getUrlData();
|
||||||
|
|
||||||
if (changed && mode !== MODES.CONFIRM_CANCEL) {
|
if (changed && mode !== MODES.CONFIRM_CANCEL) {
|
||||||
yield put(editorSetMode(MODES.CONFIRM_CANCEL));
|
yield put(editorChangeMode(MODES.CONFIRM_CANCEL));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
yield put(editorSetChanged(false));
|
yield put(editorSetChanged(false));
|
||||||
yield put(editorSetReady(true));
|
yield put(editorSetReady(true));
|
||||||
|
|
||||||
|
@ -97,10 +99,10 @@ export function* setReadySaga() {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
yield call(checkOSRMServiceSaga);
|
yield call(checkOSRMServiceSaga);
|
||||||
|
|
||||||
// TODO: someday make nominatim, but sorted by nearest points.
|
// TODO: someday make nominatim, but sorted by nearest points.
|
||||||
// yield call(checkNominatimSaga);
|
// yield call(checkNominatimSaga);
|
||||||
|
|
||||||
yield put(searchSetTab(TABS.MY));
|
yield put(searchSetTab(TABS.MY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,11 +146,11 @@ function* takeAShotSaga() {
|
||||||
timeout: delay(500),
|
timeout: delay(500),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (timeout) yield put(editorSetMode(MODES.SHOT_PREFETCH));
|
if (timeout) yield put(editorChangeMode(MODES.SHOT_PREFETCH));
|
||||||
|
|
||||||
const data = yield result || worker;
|
const data = yield result || worker;
|
||||||
|
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
yield put(
|
yield put(
|
||||||
editorSetRenderer({
|
editorSetRenderer({
|
||||||
data,
|
data,
|
||||||
|
@ -200,7 +202,7 @@ function* locationChangeSaga({ location }: ReturnType<typeof editorLocationChang
|
||||||
const mode: ReturnType<typeof selectEditorMode> = yield select(selectEditorMode);
|
const mode: ReturnType<typeof selectEditorMode> = yield select(selectEditorMode);
|
||||||
|
|
||||||
if (mode !== MODES.NONE) {
|
if (mode !== MODES.NONE) {
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
yield call(loadMapFromPath);
|
yield call(loadMapFromPath);
|
||||||
|
@ -221,7 +223,7 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof editorKeyPressed>) {
|
||||||
|
|
||||||
if (renderer_active) return yield put(editorHideRenderer());
|
if (renderer_active) return yield put(editorHideRenderer());
|
||||||
if (dialog_active) return yield put(editorSetDialogActive(false));
|
if (dialog_active) return yield put(editorSetDialogActive(false));
|
||||||
if (mode !== MODES.NONE) return yield put(editorSetMode(MODES.NONE));
|
if (mode !== MODES.NONE) return yield put(editorChangeMode(MODES.NONE));
|
||||||
} else if (key === 'Delete') {
|
} else if (key === 'Delete') {
|
||||||
const { editing } = yield select(selectEditor);
|
const { editing } = yield select(selectEditor);
|
||||||
|
|
||||||
|
@ -232,7 +234,7 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof editorKeyPressed>) {
|
||||||
if (mode === MODES.TRASH) {
|
if (mode === MODES.TRASH) {
|
||||||
yield put(editorClearAll());
|
yield put(editorClearAll());
|
||||||
} else {
|
} else {
|
||||||
yield put(editorSetMode(MODES.TRASH));
|
yield put(editorChangeMode(MODES.TRASH));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,7 +250,7 @@ function* getGPXTrackSaga() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* routerCancel() {
|
function* routerCancel() {
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* mapClick({ latlng }: ReturnType<typeof mapClicked>) {
|
function* mapClick({ latlng }: ReturnType<typeof mapClicked>) {
|
||||||
|
@ -268,7 +270,7 @@ function* routerSubmit() {
|
||||||
|
|
||||||
yield put(mapSetRoute([...route, ...coordinates]));
|
yield put(mapSetRoute([...route, ...coordinates]));
|
||||||
OsrmRouter.setWaypoints([]);
|
OsrmRouter.setWaypoints([]);
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* cancelSave() {
|
function* cancelSave() {
|
||||||
|
@ -290,11 +292,30 @@ function* searchNominatimSaga({ search }: ReturnType<typeof editorSearchNominati
|
||||||
yield put(editorSetNominatim({ loading: true, search }));
|
yield put(editorSetNominatim({ loading: true, search }));
|
||||||
const list = yield call(searchNominatim, search);
|
const list = yield call(searchNominatim, search);
|
||||||
yield put(editorSetNominatim({ list }));
|
yield put(editorSetNominatim({ list }));
|
||||||
|
|
||||||
yield delay(1000); // safely wait for 1s to prevent from ddosing nominatim
|
yield delay(1000); // safely wait for 1s to prevent from ddosing nominatim
|
||||||
yield put(editorSetNominatim({ loading: false }));
|
yield put(editorSetNominatim({ loading: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* changeMode({ mode }: ReturnType<typeof editorChangeMode>) {
|
||||||
|
const current: ReturnType<typeof selectEditorMode> = yield select(selectEditorMode);
|
||||||
|
|
||||||
|
if (mode === current) {
|
||||||
|
yield put(editorSetMode(MODES.NONE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (current) {
|
||||||
|
case MODES.ROUTER:
|
||||||
|
yield put(editorSetRouter({ waypoints: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(editorSetMode(mode));
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function* editorSaga() {
|
export function* editorSaga() {
|
||||||
yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga);
|
yield takeEvery(EDITOR_ACTIONS.LOCATION_CHANGED, locationChangeSaga);
|
||||||
|
|
||||||
|
@ -308,4 +329,5 @@ export function* editorSaga() {
|
||||||
yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit);
|
yield takeLatest(EDITOR_ACTIONS.ROUTER_SUBMIT, routerSubmit);
|
||||||
yield takeLatest(EDITOR_ACTIONS.CANCEL_SAVE, cancelSave);
|
yield takeLatest(EDITOR_ACTIONS.CANCEL_SAVE, cancelSave);
|
||||||
yield takeLeading(EDITOR_ACTIONS.SEARCH_NOMINATIM, searchNominatimSaga);
|
yield takeLeading(EDITOR_ACTIONS.SEARCH_NOMINATIM, searchNominatimSaga);
|
||||||
|
yield takeLeading(EDITOR_ACTIONS.CHANGE_MODE, changeMode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
import { selectUser, selectUserUser } from '~/redux/user/selectors';
|
import { selectUser, selectUserUser } from '~/redux/user/selectors';
|
||||||
import { MODES } from '~/constants/modes';
|
import { MODES } from '~/constants/modes';
|
||||||
import {
|
import {
|
||||||
editorSetMode,
|
editorChangeMode,
|
||||||
editorSetChanged,
|
editorSetChanged,
|
||||||
editorSetEditing,
|
editorSetEditing,
|
||||||
editorSetReady,
|
editorSetReady,
|
||||||
|
@ -51,10 +51,11 @@ function* onMapClick({ latlng }: ReturnType<typeof mapClicked>) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case MODES.STICKERS:
|
case MODES.STICKERS:
|
||||||
yield put(mapAddSticker({ latlng, set, sticker, text: '', angle: 2.11 }));
|
yield put(mapAddSticker({ latlng, set, sticker, text: '', angle: 2.11 }));
|
||||||
yield put(editorSetMode(MODES.NONE));
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +157,7 @@ export function* mapInitSaga() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* setActiveStickerSaga() {
|
function* setActiveStickerSaga() {
|
||||||
yield put(editorSetMode(MODES.STICKERS));
|
yield put(editorChangeMode(MODES.STICKERS));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* setTitleSaga({ title }: ReturnType<typeof mapSetTitle>) {
|
function* setTitleSaga({ title }: ReturnType<typeof mapSetTitle>) {
|
||||||
|
@ -171,20 +172,20 @@ function* startEditingSaga() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function* clearPolySaga() {
|
function* clearPolySaga() {
|
||||||
const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute)
|
const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute);
|
||||||
if (!route.length) return;
|
if (!route.length) return;
|
||||||
yield put(mapSetRoute([]));
|
yield put(mapSetRoute([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* clearStickersSaga() {
|
function* clearStickersSaga() {
|
||||||
const stickers: ReturnType<typeof selectMapStickers> = yield select(selectMapStickers)
|
const stickers: ReturnType<typeof selectMapStickers> = yield select(selectMapStickers);
|
||||||
if (!stickers.length) return;
|
if (!stickers.length) return;
|
||||||
yield put(mapSetStickers([]));
|
yield put(mapSetStickers([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* clearAllSaga() {
|
function* clearAllSaga() {
|
||||||
const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute)
|
const route: ReturnType<typeof selectMapRoute> = yield select(selectMapRoute);
|
||||||
const stickers: ReturnType<typeof selectMapStickers> = yield select(selectMapStickers)
|
const stickers: ReturnType<typeof selectMapStickers> = yield select(selectMapStickers);
|
||||||
|
|
||||||
if (!stickers.length && !route.length) return;
|
if (!stickers.length && !route.length) return;
|
||||||
|
|
||||||
|
@ -211,8 +212,15 @@ function* clearSaga({ type }) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield put(editorSetActiveSticker(null));
|
const { mode, activeSticker }: ReturnType<typeof selectEditor> = yield select(selectEditor);
|
||||||
yield put(editorSetMode(MODES.NONE));
|
|
||||||
|
if (activeSticker && activeSticker.set && activeSticker.sticker) {
|
||||||
|
yield put(editorSetActiveSticker(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode !== MODES.NONE) {
|
||||||
|
yield put(editorChangeMode(MODES.NONE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* sendSaveRequestSaga({
|
function* sendSaveRequestSaga({
|
||||||
|
@ -264,7 +272,7 @@ function* sendSaveRequestSaga({
|
||||||
|
|
||||||
yield put(editorSetSave({ loading: false }));
|
yield put(editorSetSave({ loading: false }));
|
||||||
|
|
||||||
if (cancel) return yield put(editorSetMode(MODES.NONE));
|
if (cancel) return yield put(editorChangeMode(MODES.NONE));
|
||||||
|
|
||||||
if (result && result.data.code === 'already_exist')
|
if (result && result.data.code === 'already_exist')
|
||||||
return yield put(editorSetSave({ overwriting: true }));
|
return yield put(editorSetSave({ overwriting: true }));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue