From 331823f2ece2af332004a8c33e277da653992f80 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Sun, 21 Jul 2024 20:21:32 +0200 Subject: [PATCH] add DangerousActionButton, add option to delete files thru StorageModal --- dist/index.js | 144 +++++++++++++----- dist/styles.css | 7 +- src/View/ChatPages/settingsPage.tsx | 15 +- src/View/Components/dangerousActionButton.tsx | 47 ++++++ src/View/Components/directoryItemList.tsx | 30 ++-- src/View/Components/fileBrowser.tsx | 88 +++++------ src/View/Modals/storageModal.tsx | 8 +- src/View/translations.ts | 6 +- src/ViewModel/storageViewModel.ts | 32 ++++ 9 files changed, 259 insertions(+), 118 deletions(-) create mode 100644 src/View/Components/dangerousActionButton.tsx diff --git a/dist/index.js b/dist/index.js index e0b85a99..029ef3ef 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1100,6 +1100,8 @@ general: { closeButton: "Close", deleteItemButtonAudioLabel: "delete item", + abortButton: "Abort", + confirmButton: "Confirm", setButton: "Set" }, regional: { @@ -1143,12 +1145,13 @@ /// connectButtonAudioLabel: "connect" }, - fileBrowser: { + storage: { noItemSelected: "No item selected", notAFile: "(not a file)", contentEmpty: "(empty)", path: "Path", - content: "Content" + content: "Content", + deleteItem: "Delete item" }, chatPage: { closeChatAudioLabe: "close chat", @@ -1306,6 +1309,40 @@ )))))); } + // src/View/Components/dangerousActionButton.tsx + function DangerousActionButton(label, icon, action) { + const isActionRequested = new State(false); + const cannotConfirm = createProxyState( + [isActionRequested], + () => isActionRequested.value == false + ); + function requestAction() { + isActionRequested.value = true; + } + function abort() { + isActionRequested.value = false; + } + return /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": abort, "toggle:hidden": cannotConfirm }, translations.general.abortButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "undo")), /* @__PURE__ */ createElement( + "button", + { + class: "danger flex", + "on:click": requestAction, + "toggle:hidden": isActionRequested + }, + label, + /* @__PURE__ */ createElement("span", { class: "icon" }, icon) + ), /* @__PURE__ */ createElement( + "button", + { + class: "danger flex", + "on:click": action, + "toggle:hidden": cannotConfirm + }, + translations.general.confirmButton, + /* @__PURE__ */ createElement("span", { class: "icon" }, "warning") + )); + } + // src/View/Components/deletableListItem.tsx function DeletableListItem(text, primaryButton, ondelete) { return /* @__PURE__ */ createElement("div", { class: "tile flex-row justify-apart align-center padding-0" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, text), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, primaryButton, /* @__PURE__ */ createElement( @@ -1409,15 +1446,11 @@ "on:click": setColor } ); - })), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement( - "button", - { - class: "danger width-input flex-no", - "on:click": chatViewModel.remove - }, + })), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton( translations.chatPage.settings.deleteChatButton, - /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_forever") - ))); + "chat_error", + chatViewModel.remove + )))); } // src/View/chatPage.tsx @@ -2092,15 +2125,15 @@ }; // src/View/Components/directoryItemList.tsx - function DirectoryItemList(storageModel2, pathString, selectedPath) { - const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageModel2, pathString2, selectedPath); + function DirectoryItemList(storageViewModel2, pathString = PATH_COMPONENT_SEPARATOR) { + const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageViewModel2, pathString2); const path = StorageModel.stringToPathComponents(pathString); const fileName = StorageModel.getFileName(path); const items = new ListState(); const style = `text-indent: ${path.length * 2}rem`; function loadItems() { items.clear(); - const directoryItems = storageModel2.list(path); + const directoryItems = storageViewModel2.storageModel.list(path); for (const directoryItem of directoryItems) { const itemPath = [...path, directoryItem]; const pathString2 = StorageModel.pathComponentsToString(...itemPath); @@ -2108,11 +2141,16 @@ } } function select() { - selectedPath.value = pathString; + storageViewModel2.selectedPath.value = pathString; } + storageViewModel2.lastDeletedItemPath.subscribe((lastDeletedItemPath) => { + if (!items.value.has(lastDeletedItemPath)) return; + select(); + setTimeout(() => loadItems(), 50); + }); const isSelected = createProxyState( - [selectedPath], - () => selectedPath.value == pathString + [storageViewModel2.selectedPath], + () => storageViewModel2.selectedPath.value == pathString ); isSelected.subscribe(() => { if (isSelected.value == false) return; @@ -2136,30 +2174,35 @@ } // src/View/Components/fileBrowser.tsx - function FileBrowser(storageModel2, selectedPath, didMakeChanges) { - const fileContentToShow = createProxyState([selectedPath], () => { - const path = StorageModel.stringToPathComponents(selectedPath.value); - const content = storageModel2.restore(path); - return (content ?? translations.fileBrowser.notAFile) || translations.fileBrowser.contentEmpty; - }); - const selectedFileName = createProxyState( - [selectedPath], - () => StorageModel.getFileNameFromString(selectedPath.value) ?? PATH_COMPONENT_SEPARATOR + function FileBrowser(storageViewModel2) { + const detailView = createProxyState( + [storageViewModel2.selectedPath], + () => { + if (storageViewModel2.selectedPath.value == PATH_COMPONENT_SEPARATOR) + return /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.storage.noItemSelected); + return /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.path), /* @__PURE__ */ createElement( + "span", + { + class: "break-all", + "subscribe:innerText": storageViewModel2.selectedPath + } + ))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.content), /* @__PURE__ */ createElement( + "code", + { + "subscribe:innerText": storageViewModel2.selectedFileContent + } + ))), DangerousActionButton( + translations.storage.deleteItem, + "delete_forever", + storageViewModel2.deleteSelectedItem + )); + } ); - const detailView = createProxyState([selectedPath], () => { - if (selectedPath.value == PATH_COMPONENT_SEPARATOR) - return /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.fileBrowser.noItemSelected); - return /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.fileBrowser.path), /* @__PURE__ */ createElement("span", { class: "break-all", "subscribe:innerText": selectedPath }))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.fileBrowser.content), /* @__PURE__ */ createElement("code", { "subscribe:innerText": fileContentToShow })))); - }); - const view = /* @__PURE__ */ createElement("div", { class: "file-browser" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "scroll-area" }, DirectoryItemList( - storageModel2, - PATH_COMPONENT_SEPARATOR, - selectedPath - )), /* @__PURE__ */ createElement("div", { class: "detail-button-wrapper" }, /* @__PURE__ */ createElement("button", { class: "ghost", "on:click": scrollToDetails }, /* @__PURE__ */ createElement( + const view = /* @__PURE__ */ createElement("div", { class: "file-browser" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "scroll-area" }, DirectoryItemList(storageViewModel2)), /* @__PURE__ */ createElement("div", { class: "detail-button-wrapper" }, /* @__PURE__ */ createElement("button", { class: "ghost", "on:click": scrollToDetails }, /* @__PURE__ */ createElement( "span", { class: "ellipsis", - "subscribe:innerText": selectedFileName + "subscribe:innerText": storageViewModel2.selectedFileName } ), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))), /* @__PURE__ */ createElement("div", { class: "scroll-area", "children:set": detailView })); function scrollToDetails() { @@ -2170,11 +2213,7 @@ // src/View/Modals/storageModal.tsx function StorageModal(storageViewModel2) { - return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": storageViewModel2.isShowingStorageModal }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", { class: "padding-0" }, FileBrowser( - storageViewModel2.storageModel, - storageViewModel2.selectedPath, - storageViewModel2.didMakeChanges - )), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); + return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": storageViewModel2.isShowingStorageModal }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", { class: "padding-0" }, FileBrowser(storageViewModel2)), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); } // src/ViewModel/storageViewModel.ts @@ -2184,6 +2223,21 @@ isShowingStorageModal = new State(false); selectedPath = new State(PATH_COMPONENT_SEPARATOR); didMakeChanges = new State(false); + selectedFileName; + selectedFileContent; + lastDeletedItemPath = new State(""); + // methods + getSelectedItemContent = () => { + const path = StorageModel.stringToPathComponents(this.selectedPath.value); + const content = this.storageModel.restore(path); + return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty; + }; + deleteSelectedItem = () => { + const path = StorageModel.stringToPathComponents(this.selectedPath.value); + this.lastDeletedItemPath.value = this.selectedPath.value; + this.storageModel.removeRecursive(path); + this.didMakeChanges.value = true; + }; // view methods showStorageModal = () => { this.isShowingStorageModal.value = true; @@ -2197,6 +2251,14 @@ // init constructor(storageModel2) { this.storageModel = storageModel2; + this.selectedFileName = createProxyState( + [this.selectedPath], + () => StorageModel.getFileNameFromString(this.selectedPath.value) + ); + this.selectedFileContent = createProxyState( + [this.selectedPath], + () => this.getSelectedItemContent() + ); } }; diff --git a/dist/styles.css b/dist/styles.css index 1b249856..b19a8517 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -457,11 +457,14 @@ textarea { display: inline-flex; } -.modal, -[hidden] { +.modal{ display: none; } +[hidden] { + display: none !important; +} + main, .tile, .tile > div { diff --git a/src/View/ChatPages/settingsPage.tsx b/src/View/ChatPages/settingsPage.tsx index 3eda6da6..ee97c114 100644 --- a/src/View/ChatPages/settingsPage.tsx +++ b/src/View/ChatPages/settingsPage.tsx @@ -2,6 +2,7 @@ import * as React from "bloatless-react"; import ChatViewModel from "../../ViewModel/chatViewModel"; import { Color } from "../../ViewModel/colors"; +import { DangerousActionButton } from "../Components/dangerousActionButton"; import { DeletableListItem } from "../Components/deletableListItem"; import { translations } from "../translations"; @@ -139,13 +140,13 @@ export function SettingsPage(chatViewModel: ChatViewModel) {
- +
+ {DangerousActionButton( + translations.chatPage.settings.deleteChatButton, + "chat_error", + chatViewModel.remove + )} +
); diff --git a/src/View/Components/dangerousActionButton.tsx b/src/View/Components/dangerousActionButton.tsx new file mode 100644 index 00000000..2a9a06c6 --- /dev/null +++ b/src/View/Components/dangerousActionButton.tsx @@ -0,0 +1,47 @@ +import * as React from "bloatless-react"; + +import { translations } from "../translations"; + +export function DangerousActionButton( + label: string, + icon: string, + action: () => void +) { + const isActionRequested = new React.State(false); + const cannotConfirm = React.createProxyState( + [isActionRequested], + () => isActionRequested.value == false + ); + + function requestAction() { + isActionRequested.value = true; + } + function abort() { + isActionRequested.value = false; + } + + return ( +
+ + + +
+ ); +} diff --git a/src/View/Components/directoryItemList.tsx b/src/View/Components/directoryItemList.tsx index 4fe32f55..46c90e57 100644 --- a/src/View/Components/directoryItemList.tsx +++ b/src/View/Components/directoryItemList.tsx @@ -1,17 +1,19 @@ import * as React from "bloatless-react"; -import StorageModel from "../../Model/storageModel"; -import { translations } from "../translations"; +import StorageModel, { + PATH_COMPONENT_SEPARATOR, +} from "../../Model/storageModel"; + +import StorageViewModel from "../../ViewModel/storageViewModel"; export function DirectoryItemList( - storageModel: StorageModel, - pathString: string, - selectedPath: React.State + storageViewModel: StorageViewModel, + pathString: string = PATH_COMPONENT_SEPARATOR ) { // converter const StringToDirectoryItemList: React.StateItemConverter = ( pathString: string - ) => DirectoryItemList(storageModel, pathString, selectedPath); + ) => DirectoryItemList(storageViewModel, pathString); // data const path = StorageModel.stringToPathComponents(pathString); @@ -24,7 +26,7 @@ export function DirectoryItemList( function loadItems() { items.clear(); - const directoryItems = storageModel.list(path); + const directoryItems = storageViewModel.storageModel.list(path); for (const directoryItem of directoryItems) { const itemPath = [...path, directoryItem]; @@ -34,13 +36,21 @@ export function DirectoryItemList( } function select() { - selectedPath.value = pathString; + storageViewModel.selectedPath.value = pathString; } + // handlers + storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => { + if (!items.value.has(lastDeletedItemPath)) return; + + select(); + setTimeout(() => loadItems(), 50) + }); + // state const isSelected = React.createProxyState( - [selectedPath], - () => selectedPath.value == pathString + [storageViewModel.selectedPath], + () => storageViewModel.selectedPath.value == pathString ); isSelected.subscribe(() => { diff --git a/src/View/Components/fileBrowser.tsx b/src/View/Components/fileBrowser.tsx index ea659c96..597d73e0 100644 --- a/src/View/Components/fileBrowser.tsx +++ b/src/View/Components/fileBrowser.tsx @@ -6,71 +6,59 @@ import StorageModel, { PATH_COMPONENT_SEPARATOR, } from "../../Model/storageModel"; +import { DangerousActionButton } from "./dangerousActionButton"; import { DirectoryItemList } from "./directoryItemList"; +import StorageViewModel from "../../ViewModel/storageViewModel"; import { translations } from "../translations"; -export function FileBrowser( - storageModel: StorageModel, - selectedPath: React.State, - didMakeChanges: React.State -) { - // state - const fileContentToShow = React.createProxyState([selectedPath], () => { - const path = StorageModel.stringToPathComponents(selectedPath.value); - const content = storageModel.restore(path); - return ( - (content ?? translations.fileBrowser.notAFile) || - translations.fileBrowser.contentEmpty - ); - }); +export function FileBrowser(storageViewModel: StorageViewModel) { + const detailView = React.createProxyState( + [storageViewModel.selectedPath], + () => { + if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR) + return ( + {translations.storage.noItemSelected} + ); - const selectedFileName = React.createProxyState( - [selectedPath], - () => - StorageModel.getFileNameFromString(selectedPath.value) ?? - PATH_COMPONENT_SEPARATOR - ); - - // view - const detailView = React.createProxyState([selectedPath], () => { - if (selectedPath.value == PATH_COMPONENT_SEPARATOR) return ( - {translations.fileBrowser.noItemSelected} - ); +
+
+
+ {translations.storage.path} + +
+
+
+
+ {translations.storage.content} + +
+
- return ( -
-
-
- {translations.fileBrowser.path} - -
+ {DangerousActionButton( + translations.storage.deleteItem, + "delete_forever", + storageViewModel.deleteSelectedItem + )}
-
-
- {translations.fileBrowser.content} - -
-
-
- ); - }); + ); + } + ); const view = (
-
- {DirectoryItemList( - storageModel, - PATH_COMPONENT_SEPARATOR, - selectedPath - )} -
+
{DirectoryItemList(storageViewModel)}
diff --git a/src/View/Modals/storageModal.tsx b/src/View/Modals/storageModal.tsx index 99e08b66..263e2cad 100644 --- a/src/View/Modals/storageModal.tsx +++ b/src/View/Modals/storageModal.tsx @@ -8,13 +8,7 @@ export function StorageModal(storageViewModel: StorageViewModel) { return (