diff --git a/api/obs/api/routes/users.py b/api/obs/api/routes/users.py index 38ffaf1..ceb0efc 100644 --- a/api/obs/api/routes/users.py +++ b/api/obs/api/routes/users.py @@ -1,8 +1,8 @@ import logging from sanic.response import json -from sanic.exceptions import InvalidUsage, Forbidden -from sqlalchemy import select +from sanic.exceptions import InvalidUsage, Forbidden, NotFound +from sqlalchemy import and_, select from obs.api.app import api, require_auth from obs.api.db import UserDevice @@ -46,6 +46,32 @@ async def get_user_devices(req): return json([device.to_dict(req.ctx.user.id) for device in devices]) +@api.put("/user/devices/") +async def put_user_device(req, device_id): + if not req.ctx.user: + raise Forbidden() + + body = req.json + + query = ( + select(UserDevice) + .where(and_(UserDevice.user_id == req.ctx.user.id, UserDevice.id == device_id)) + .limit(1) + ) + + device = (await req.ctx.db.execute(query)).scalar() + + if device is None: + raise NotFound() + + new_name = body.get("displayName", "").strip() + if new_name and device.display_name != new_name: + device.display_name = new_name + await req.ctx.db.commit() + + return json(device.to_dict()) + + @api.put("/user") @require_auth async def put_user(req): diff --git a/frontend/src/pages/SettingsPage/DeviceList.tsx b/frontend/src/pages/SettingsPage/DeviceList.tsx new file mode 100644 index 0000000..0ab91eb --- /dev/null +++ b/frontend/src/pages/SettingsPage/DeviceList.tsx @@ -0,0 +1,126 @@ +import React, {useCallback, useMemo, useRef} from 'react' +import {useObservable} from 'rxjs-hooks' +import {concat, from, of, Subject} from 'rxjs' +import {Table, Button, Input} from 'semantic-ui-react' +import {useTranslation} from 'react-i18next' + +import api from 'api' +import {UserDevice} from 'types' +import {startWith, switchMap} from 'rxjs/operators' + +function EditField({value, onEdit}) { + const [editing, setEditing] = React.useState(false) + const [tempValue, setTempValue] = React.useState(value) + const timeoutRef = useRef(null) + + const cancelTimeout = useCallback(() => { + if (timeoutRef.current != null) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, [timeoutRef]) + + const abort = useCallback(() => { + cancelTimeout() + setEditing(false) + setTempValue(value) + }, [setEditing, setTempValue, value, cancelTimeout]) + + const confirm = useCallback(() => { + cancelTimeout() + setEditing(false) + onEdit(tempValue) + }, [setEditing, onEdit, tempValue, cancelTimeout]) + + React.useEffect(() => { + if (value !== tempValue) { + setTempValue(value) + } + }, [value]) + + if (editing) { + return ( + <> + setTempValue(e.target.value)} + onBlur={(e) => { + timeoutRef.current = setTimeout(abort, 20) + }} + onKeyPress={(e) => { + if (e.key === 'Enter') { + confirm() + } else if (e.key === 'Escape') { + abort() + } + }} + style={{marginRight: 8}} + /> +