From d7b8ad997675a020e00f33b30daa21f7df779216 Mon Sep 17 00:00:00 2001 From: Fritz Heiden Date: Tue, 15 Apr 2025 18:47:43 +0200 Subject: [PATCH] feat: add editing of remotes --- data/remote_database.go | 30 ++++ management/remote_manager.go | 4 + server/remote_api_handler.go | 19 ++- www/src/components/list-manager.jsx | 21 ++- www/src/data/remote.js | 2 +- www/src/modals/edit-remote-modal.jsx | 145 ++++++++++++++++++++ www/src/modals/modal-registry.jsx | 6 + www/src/services/remotes-service.js | 16 +++ www/src/views/remotes/remotes-list-view.jsx | 21 ++- 9 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 www/src/modals/edit-remote-modal.jsx diff --git a/data/remote_database.go b/data/remote_database.go index 8c38eca..cc68616 100644 --- a/data/remote_database.go +++ b/data/remote_database.go @@ -97,6 +97,19 @@ func (db *RemoteDatabase) CreateRemoteCommands(remoteId string, commandIds []str return nil } +func (db *RemoteDatabase) UpdateRemoteCommands(remoteId string, commandIds []string) error { + queryString := "DELETE FROM RemoteCommands WHERE remote_id = ?" + _, err := db.Connection.Exec(queryString, remoteId) + if err != nil { + return err + } + err = db.CreateRemoteCommands(remoteId, commandIds) + if err != nil { + return err + } + return nil +} + func (db *RemoteDatabase) GetRemote(remoteId string) (Remote, error) { var remote Remote queryString := "SELECT id, title FROM Remotes WHERE id = ?" @@ -132,6 +145,23 @@ func (db *RemoteDatabase) GetRemotes() ([]Remote, error) { return remotes, nil } +func (db *RemoteDatabase) UpdateRemote(remote Remote) error { + queryString := "UPDATE Remotes SET title = ? WHERE id = ?" + _, error := db.Connection.Exec(queryString, remote.Title, remote.Id) + if error != nil { + return fmt.Errorf("error updating remote %s: %s", remote.Id, error) + } + commandIds := []string{} + for _, command := range remote.Commands { + commandIds = append(commandIds, command.Id) + } + err := db.UpdateRemoteCommands(remote.Id, commandIds) + if err != nil { + return err + } + return nil +} + func (db *RemoteDatabase) DeleteRemote(remoteId string) error { queryString := "DELETE FROM Remotes WHERE id = ?" _, error := db.Connection.Exec(queryString, remoteId) diff --git a/management/remote_manager.go b/management/remote_manager.go index f1cbe44..1d97581 100644 --- a/management/remote_manager.go +++ b/management/remote_manager.go @@ -33,6 +33,10 @@ func (rm *RemoteManager) GetRemotes() ([]d.Remote, error) { return remotes, nil } +func (rm *RemoteManager) UpdateRemote(remote d.Remote) error { + return rm.remoteDatabase.UpdateRemote(remote) +} + func (rm *RemoteManager) DeleteRemote(remoteID string) error { return rm.remoteDatabase.DeleteRemote(remoteID) } diff --git a/server/remote_api_handler.go b/server/remote_api_handler.go index 1164b8c..d5b663a 100644 --- a/server/remote_api_handler.go +++ b/server/remote_api_handler.go @@ -19,7 +19,8 @@ func (r *RemoteApiHandler) Initialize(authenticator *Authenticator) { remotesApi.GET("", r.handleGetRemotes) remotesApi.GET("/:id", r.handleGetRemote) remotesApi.POST("", r.handleCreateRemote) - remotesApi.DELETE("/:id", r.handleDelteRemote) + remotesApi.PUT("/:id", r.handleUpdateRemote) + remotesApi.DELETE("/:id", r.handleDeleteRemote) r.router.Use(authenticator.Authenticate("/api/commands", []string{})) commandsApi := r.router.Group("/api/commands") @@ -65,7 +66,21 @@ func (r *RemoteApiHandler) handleGetRemotes(context echo.Context) error { return context.JSON(200, remotes) } -func (r *RemoteApiHandler) handleDelteRemote(context echo.Context) error { +func (r *RemoteApiHandler) handleUpdateRemote(context echo.Context) error { + remote := d.Remote{} + if err := context.Bind(&remote); err != nil { + SendError(400, context, err.Error()) + return nil + } + err := r.remoteManager.UpdateRemote(remote) + if err != nil { + SendError(500, context, err.Error()) + return err + } + return context.JSON(200, "") +} + +func (r *RemoteApiHandler) handleDeleteRemote(context echo.Context) error { id := context.Param("id") err := r.remoteManager.DeleteRemote(id) if err != nil { diff --git a/www/src/components/list-manager.jsx b/www/src/components/list-manager.jsx index 4549a05..a89b7cc 100644 --- a/www/src/components/list-manager.jsx +++ b/www/src/components/list-manager.jsx @@ -1,21 +1,23 @@ -import { createEffect, createMemo, createSignal, mergeProps } from "solid-js"; +import { createMemo, createSignal, mergeProps } from "solid-js"; function ListManager(props) { props = mergeProps( { items: [], availableItems: [], - itemToString: () => "", style: "", - onItemSelect: () => {}, - onItemDeselect: () => {}, itemsTitle: "Selected items", availableItemsTitle: "Available items", + itemToString: () => "", + onItemSelect: () => {}, + onItemDeselect: () => {}, + itemsEqual: (a, b) => a === b, }, props ); const itemToString = (item) => props.itemToString(item); + const byLabel = (a, b) => itemToString(a).localeCompare(itemToString(b)); const [selectedAvailableItemIndex, setSelectedAvailableItemIndex] = createSignal(-1); const [selectedItemIndex, setSelectedItemIndex] = createSignal(-1); @@ -42,11 +44,16 @@ function ListManager(props) { .search(availableItemsSearchString()) .map((item) => item.item) : props.availableItems - ).filter((item) => !props.items.includes(item)) + ).filter( + (availableItem) => + !props.items.find((item) => props.itemsEqual(item, availableItem)) + ) ); const selectableItems = createMemo(() => itemsSearchString() - ? itemsFuse().search(itemsSearchString()).map((item) => item.item) + ? itemsFuse() + .search(itemsSearchString()) + .map((item) => item.item) : props.items ); const canSelect = createMemo( @@ -120,7 +127,7 @@ function ListManager(props) { />
- {props.items.map((item, index) => ( + {props.items.sort(byLabel).map((item, index) => ( props.onItemSelected(index)} selected={index === props.selectedItemIndex} diff --git a/www/src/data/remote.js b/www/src/data/remote.js index 8782ecc..00027b5 100644 --- a/www/src/data/remote.js +++ b/www/src/data/remote.js @@ -1,4 +1,4 @@ -function Remote({ id, title, commands = [] } = {}) { +function Remote({ id = "", title = "", commands = [] } = {}) { let _id = id; let _title = title; let _commands = commands; diff --git a/www/src/modals/edit-remote-modal.jsx b/www/src/modals/edit-remote-modal.jsx new file mode 100644 index 0000000..34a75ce --- /dev/null +++ b/www/src/modals/edit-remote-modal.jsx @@ -0,0 +1,145 @@ +import { + createEffect, + createMemo, + createResource, + createSignal, +} from "solid-js"; +import ListManager from "../components/list-manager.jsx"; +import ValidatedTextInput from "../components/validated-text-input.jsx"; +import Remote from "../data/remote.js"; +import RemoteService from "../services/remotes-service.js"; +import EventEmitter from "../tools/event-emitter.js"; +import ModalHandler from "./modal-handler.js"; +import Modal from "./modal.jsx"; + +const eventEmitter = new EventEmitter(); +const REMOTE_EDITED_EVENT = "success"; +const MIN_TITLE_LENGTH = 3; +const modalHandler = new ModalHandler(); + +const [remoteId, setRemoteId] = createSignal(null); + +function EditRemoteModal(props) { + const [title, setTitle] = createSignal(""); + const [commands, setCommands] = createSignal([]); + const [availableCommands, { refetch: refetchAvailableCommands }] = + createResource(RemoteService.getCommands); + const [remote, {}] = createResource( + remoteId, + () => RemoteService.getRemote(remoteId()), + { initialValue: new Remote() } + ); + const [error, setError] = createSignal(""); + + const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH); + const isFormValid = createMemo(() => isTitleValid()); + + createEffect(() => { + if (!remote()) return; + setTitle(remote().getTitle()); + setCommands(remote().getCommands()); + }); + + modalHandler.onShow(() => { + refetchAvailableCommands(); + }); + + async function handleEditRemote() { + try { + await RemoteService.updateRemote( + new Remote({ + id: remote().getId(), + title: title(), + commands: commands(), + }) + ); + } catch (e) { + setError(e.message); + console.error(e); + return; + } + resetFields(); + EditRemoteModal.Handler.hide(); + eventEmitter.dispatchEvent(REMOTE_EDITED_EVENT); + } + + function resetFields() { + setTitle(""); + setCommands([]); + setRemoteId(""); + setError(""); + } + + function handleCommandSelect(item) { + setCommands([...commands(), item]); + } + + function handleCommandDeselect(item) { + setCommands( + commands().filter((command) => command.getId() !== item.getId()) + ); + } + + return ( + + + + + ); +} + +EditRemoteModal.Handler = modalHandler; +EditRemoteModal.onRemoteEdited = (callback) => + eventEmitter.on(REMOTE_EDITED_EVENT, callback); +EditRemoteModal.setRemoteId = setRemoteId; + +export default EditRemoteModal; diff --git a/www/src/modals/modal-registry.jsx b/www/src/modals/modal-registry.jsx index fe9d299..94eb6f1 100644 --- a/www/src/modals/modal-registry.jsx +++ b/www/src/modals/modal-registry.jsx @@ -12,6 +12,7 @@ import CreateRemoteModal from "./create-remote-modal.jsx"; import DeleteRemoteModal from "./delete-remote-modal.jsx"; import ImportCommandsModal from "./import-commands-modal.jsx"; import EditCommandModal from "./edit-command-modal.jsx"; +import EditRemoteModal from "./edit-remote-modal.jsx"; const ModalRegistry = (function () { const modals = [ @@ -75,6 +76,11 @@ const ModalRegistry = (function () { component: EditCommandModal, ref: null, }, + { + id: "editRemoteModal", + component: EditRemoteModal, + ref: null, + } ]; function getModals(props) { diff --git a/www/src/services/remotes-service.js b/www/src/services/remotes-service.js index e40523d..bd1abdd 100644 --- a/www/src/services/remotes-service.js +++ b/www/src/services/remotes-service.js @@ -6,6 +6,7 @@ const MESSAGE_TYPE_COMMAND = "command"; function RemoteService() { async function getRemote(remoteId) { + if (!remoteId) return null; let response = await Net.sendRequest({ method: "GET", url: "/api/remotes/" + remoteId, @@ -49,6 +50,20 @@ function RemoteService() { } } + async function updateRemote(remote) { + let remoteObject = Serializer.serializeRemote(remote); + let response = await Net.sendJsonRequest({ + method: "PUT", + url: "/api/remotes/" + remote.getId(), + data: remoteObject, + }); + + if (response.status !== 200) { + let responseData = JSON.parse(response.data); + throw new Error(responseData.error); + } + } + async function deleteRemote(remoteId) { let response = await Net.sendRequest({ method: "DELETE", @@ -132,6 +147,7 @@ function RemoteService() { getRemote, getRemotes, createRemote, + updateRemote, deleteRemote, getCommands, createCommand, diff --git a/www/src/views/remotes/remotes-list-view.jsx b/www/src/views/remotes/remotes-list-view.jsx index 66075b7..91dd767 100644 --- a/www/src/views/remotes/remotes-list-view.jsx +++ b/www/src/views/remotes/remotes-list-view.jsx @@ -6,6 +6,7 @@ import DeleteRemoteModal from "../../modals/delete-remote-modal"; import FileUtils from "../../tools/file-utils"; import Serializer from "../../data/serializer"; import RemoteService from "../../services/remotes-service"; +import EditRemoteModal from "../../modals/edit-remote-modal"; function RemotesList(props) { const [remotes, { refetch: refetchRemotes }] = createResource( @@ -26,10 +27,19 @@ function RemotesList(props) { refetchRemotes(); }); + EditRemoteModal.onRemoteEdited(() => { + refetchRemotes(); + }); + function handleNewRemote() { CreateRemoteModal.Handler.show(); } + function handleEditRemote(remote) { + EditRemoteModal.setRemoteId(remote.getId()); + EditRemoteModal.Handler.show(); + } + function handleDeleteRemote(remote) { DeleteRemoteModal.setRemote(remote); DeleteRemoteModal.Handler.show(); @@ -90,7 +100,7 @@ function RemotesList(props) { { id: "options", name: "", - width: 4, + width: 6, }, ]} onListItemClick={() => {}} @@ -105,6 +115,15 @@ function RemotesList(props) { options: { html: ( <> +