mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-12 22:46:17 +08:00
Set up confirmation modals (#1033)
* Set up confirmation modals * Add temporary fix for the global hook remount
This commit is contained in:
parent
f58f9609d2
commit
d191b7eb9d
17 changed files with 305 additions and 108 deletions
|
@ -34,11 +34,11 @@
|
||||||
/* Animations */
|
/* Animations */
|
||||||
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fade-in-frames 200ms;
|
animation: fade-in-frames 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-out {
|
.fade-out {
|
||||||
animation: fade-out-frames 200ms;
|
animation: fade-out-frames 200ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in-frames {
|
@keyframes fade-in-frames {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import Highlight from "./highlight";
|
||||||
import DragAndDrop from "./drag_and_drop";
|
import DragAndDrop from "./drag_and_drop";
|
||||||
import PasswordToggle from "./password_toggle";
|
import PasswordToggle from "./password_toggle";
|
||||||
import KeyboardControl from "./keyboard_control";
|
import KeyboardControl from "./keyboard_control";
|
||||||
|
import ConfirmModal from "./confirm_modal";
|
||||||
import morphdomCallbacks from "./morphdom_callbacks";
|
import morphdomCallbacks from "./morphdom_callbacks";
|
||||||
import JSView from "./js_view";
|
import JSView from "./js_view";
|
||||||
import { loadUserData } from "./lib/user";
|
import { loadUserData } from "./lib/user";
|
||||||
|
@ -46,6 +47,7 @@ const hooks = {
|
||||||
PasswordToggle,
|
PasswordToggle,
|
||||||
KeyboardControl,
|
KeyboardControl,
|
||||||
JSView,
|
JSView,
|
||||||
|
ConfirmModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const csrfToken = document
|
const csrfToken = document
|
||||||
|
|
63
assets/js/confirm_modal/index.js
Normal file
63
assets/js/confirm_modal/index.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { load, store } from "../lib/storage";
|
||||||
|
|
||||||
|
const OPT_OUT_IDS_KEY = "confirm-opted-out-ids";
|
||||||
|
|
||||||
|
const ConfirmModal = {
|
||||||
|
mounted() {
|
||||||
|
let confirmEvent = null;
|
||||||
|
|
||||||
|
const titleEl = this.el.querySelector("[data-title]");
|
||||||
|
const descriptionEl = this.el.querySelector("[data-description]");
|
||||||
|
const confirmIconEl = this.el.querySelector("[data-confirm-icon]");
|
||||||
|
const confirmTextEl = this.el.querySelector("[data-confirm-text]");
|
||||||
|
const optOutEl = this.el.querySelector("[data-opt-out]");
|
||||||
|
const optOutElCheckbox = optOutEl.querySelector("input");
|
||||||
|
|
||||||
|
const optedOutIds = load(OPT_OUT_IDS_KEY) || [];
|
||||||
|
|
||||||
|
this.handleConfirmRequest = (event) => {
|
||||||
|
confirmEvent = event;
|
||||||
|
|
||||||
|
const { title, description, confirm_text, confirm_icon, opt_out_id } =
|
||||||
|
event.detail;
|
||||||
|
|
||||||
|
if (opt_out_id && optedOutIds.includes(opt_out_id)) {
|
||||||
|
liveSocket.execJS(event.target, event.detail.on_confirm);
|
||||||
|
} else {
|
||||||
|
titleEl.textContent = title;
|
||||||
|
descriptionEl.textContent = description;
|
||||||
|
confirmTextEl.textContent = confirm_text;
|
||||||
|
|
||||||
|
if (confirm_icon) {
|
||||||
|
confirmIconEl.className = `align-middle mr-1 ri-${confirm_icon}`;
|
||||||
|
} else {
|
||||||
|
confirmIconEl.className = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
optOutElCheckbox.checked = false;
|
||||||
|
optOutEl.classList.toggle("hidden", !opt_out_id);
|
||||||
|
|
||||||
|
liveSocket.execJS(this.el, this.el.getAttribute("data-js-show"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("lb:confirm_request", this.handleConfirmRequest);
|
||||||
|
|
||||||
|
this.el.addEventListener("lb:confirm", (event) => {
|
||||||
|
const { opt_out_id } = confirmEvent.detail;
|
||||||
|
|
||||||
|
if (opt_out_id && optOutElCheckbox.checked) {
|
||||||
|
optedOutIds.push(opt_out_id);
|
||||||
|
store(OPT_OUT_IDS_KEY, optedOutIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
liveSocket.execJS(confirmEvent.target, confirmEvent.detail.on_confirm);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
window.removeEventListener("lb:confirm_request", this.handleConfirmRequest);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmModal;
|
|
@ -1,4 +1,6 @@
|
||||||
const SETTINGS_KEY = "livebook:settings";
|
import { load, store } from "./storage";
|
||||||
|
|
||||||
|
const SETTINGS_KEY = "settings";
|
||||||
|
|
||||||
export const EDITOR_FONT_SIZE = {
|
export const EDITOR_FONT_SIZE = {
|
||||||
normal: 14,
|
normal: 14,
|
||||||
|
@ -61,25 +63,15 @@ class SettingsStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadSettings() {
|
_loadSettings() {
|
||||||
try {
|
const settings = load(SETTINGS_KEY);
|
||||||
const json = localStorage.getItem(SETTINGS_KEY);
|
|
||||||
|
|
||||||
if (json) {
|
if (settings) {
|
||||||
const settings = JSON.parse(json);
|
this._settings = { ...this._settings, ...settings };
|
||||||
this._settings = { ...this._settings, ...settings };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load local settings, reason: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeSettings() {
|
_storeSettings() {
|
||||||
try {
|
store(SETTINGS_KEY, this._settings);
|
||||||
const json = JSON.stringify(this._settings);
|
|
||||||
localStorage.setItem(SETTINGS_KEY, json);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to store local settings, reason: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
assets/js/lib/storage.js
Normal file
34
assets/js/lib/storage.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
const PREFIX = "livebook:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads value from local storage.
|
||||||
|
*/
|
||||||
|
export function load(key) {
|
||||||
|
try {
|
||||||
|
const json = localStorage.getItem(PREFIX + key);
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
return JSON.parse(json);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to load from local storage, reason: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores value in local storage.
|
||||||
|
*
|
||||||
|
* The value is serialized as JSON.
|
||||||
|
*/
|
||||||
|
export function store(key, value) {
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(value);
|
||||||
|
localStorage.setItem(PREFIX + key, json);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to write to local storage, reason: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,44 +10,48 @@ defmodule LivebookWeb.Helpers do
|
||||||
@doc """
|
@doc """
|
||||||
Wraps the given content in a modal dialog.
|
Wraps the given content in a modal dialog.
|
||||||
|
|
||||||
When closed, the modal redirects to the given `:return_to` URL.
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<.modal return_to={...}>
|
<.modal id="edit-modal" patch={...}>
|
||||||
<.live_component module={MyComponent} />
|
<.live_component module={MyComponent} />
|
||||||
</.modal>
|
</.modal>
|
||||||
"""
|
"""
|
||||||
def modal(assigns) do
|
def modal(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
|
|> assign_new(:show, fn -> false end)
|
||||||
|
|> assign_new(:patch, fn -> nil end)
|
||||||
|
|> assign_new(:navigate, fn -> nil end)
|
||||||
|> assign_new(:class, fn -> "" end)
|
|> assign_new(:class, fn -> "" end)
|
||||||
|
|> assign(:attrs, assigns_to_attributes(assigns, [:id, :show, :patch, :navigate, :class]))
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="fixed z-[10000] inset-0 fade-in" phx-remove={JS.transition("fade-out")}>
|
<div id={@id} class={"fixed z-[10000] inset-0 #{if @show, do: "fade-in", else: "hidden"}"} phx-remove={JS.transition("fade-out")} {@attrs}>
|
||||||
<!-- Modal container -->
|
<!-- Modal container -->
|
||||||
<div class="h-screen flex items-center justify-center p-4">
|
<div class="h-screen flex items-center justify-center p-4">
|
||||||
<!-- Overlay -->
|
<!-- Overlay -->
|
||||||
<div class="absolute inset-0 bg-gray-500 opacity-75 z-0" aria-hidden="true"></div>
|
<div class="absolute z-0 inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- Modal box -->
|
<!-- Modal box -->
|
||||||
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@class}"}
|
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@class}"}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
autofocus
|
autofocus
|
||||||
phx-window-keydown={click_modal_close()}
|
phx-window-keydown={hide_modal(@id)}
|
||||||
phx-click-away={click_modal_close()}
|
phx-click-away={hide_modal(@id)}
|
||||||
phx-key="escape">
|
phx-key="escape">
|
||||||
|
<%= if @patch do %>
|
||||||
<%= live_patch to: @return_to,
|
<%= live_patch "", to: @patch, class: "hidden", id: "#{@id}-return" %>
|
||||||
class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center",
|
<% end %>
|
||||||
aria_label: "close modal",
|
<%= if @navigate do %>
|
||||||
id: "close-modal-button" do %>
|
<%= live_redirect "", to: @navigate, class: "hidden", id: "#{@id}-return" %>
|
||||||
|
<% end %>
|
||||||
|
<button class="absolute top-6 right-6 text-gray-400 flex space-x-1 items-center"
|
||||||
|
aria_label="close modal"
|
||||||
|
phx-click={hide_modal(@id)}>
|
||||||
<span class="text-sm">(esc)</span>
|
<span class="text-sm">(esc)</span>
|
||||||
<.remix_icon icon="close-line" class="text-2xl" />
|
<.remix_icon icon="close-line" class="text-2xl" />
|
||||||
<% end %>
|
</button>
|
||||||
|
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,8 +59,118 @@ defmodule LivebookWeb.Helpers do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp click_modal_close(js \\ %JS{}) do
|
@doc """
|
||||||
JS.dispatch(js, "click", to: "#close-modal-button")
|
Shows a modal rendered with `modal/1`.
|
||||||
|
"""
|
||||||
|
def show_modal(js \\ %JS{}, id) do
|
||||||
|
js
|
||||||
|
|> JS.show(
|
||||||
|
to: "##{id}",
|
||||||
|
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hides a modal rendered with `modal/1`.
|
||||||
|
"""
|
||||||
|
def hide_modal(js \\ %JS{}, id) do
|
||||||
|
js
|
||||||
|
|> JS.hide(
|
||||||
|
to: "##{id}",
|
||||||
|
transition: {"ease-in duration-200", "opacity-100", "opacity-0"}
|
||||||
|
)
|
||||||
|
|> JS.dispatch("click", to: "##{id}-return")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders the confirmation modal for `with_confirm/3`.
|
||||||
|
"""
|
||||||
|
def confirm_modal(assigns) do
|
||||||
|
# TODO: this ensures unique ids when navigating across LVs.
|
||||||
|
# Remove once https://github.com/phoenixframework/phoenix_live_view/issues/1903
|
||||||
|
# is resolved
|
||||||
|
lv_id = self() |> :erlang.term_to_binary() |> Base.encode32()
|
||||||
|
assigns = assign_new(assigns, :id, fn -> "confirm-modal-#{lv_id}" end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<.modal id={@id} class="w-full max-w-xl" phx-hook="ConfirmModal" data-js-show={show_modal(@id)}>
|
||||||
|
<div id={"#{@id}-content"} class="p-6 pb-4 flex flex-col" phx-update="ignore">
|
||||||
|
<h3 class="text-2xl font-semibold text-gray-800" data-title></h3>
|
||||||
|
<p class="mt-8 text-gray-700" data-description></p>
|
||||||
|
<label class="mt-6 text-gray-700 flex items-center" data-opt-out>
|
||||||
|
<input class="checkbox-base mr-3" type="checkbox" />
|
||||||
|
<span class="text-sm">
|
||||||
|
Don't show this message again
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="mt-8 flex justify-end space-x-2">
|
||||||
|
<button class="button-base button-red"
|
||||||
|
phx-click={hide_modal(@id) |> JS.dispatch("lb:confirm", to: "##{@id}")}>
|
||||||
|
<i aria-hidden="true" data-confirm-icon></i>
|
||||||
|
<span data-confirm-text></span>
|
||||||
|
</button>
|
||||||
|
<button class="button-base button-outlined-gray"
|
||||||
|
phx-click={hide_modal(@id)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</.modal>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Shows a confirmation modal before executing the given JS action.
|
||||||
|
|
||||||
|
The modal template must already be on the page, see `confirm_modal/1`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:title` - title of the confirmation modal. Defaults to `"Are you sure?"`
|
||||||
|
|
||||||
|
* `:description` - content of the confirmation modal. Required
|
||||||
|
|
||||||
|
* `:confirm_text` - text of the confirm button. Defaults to `"Yes"`
|
||||||
|
|
||||||
|
* `:confirm_icon` - icon in the confirm button. Optional
|
||||||
|
|
||||||
|
* `:opt_out_id` - enables the "Don't show this message again"
|
||||||
|
checkbox. Once checked by the user, the confirmation with this
|
||||||
|
id is never shown again. Optional
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<button class="..."
|
||||||
|
phx-click={
|
||||||
|
with_confirm(
|
||||||
|
JS.push("delete_item", value: %{id: @item_id}),
|
||||||
|
title: "Delete item",
|
||||||
|
description: "Are you sure you want to delete item?",
|
||||||
|
confirm_text: "Delete",
|
||||||
|
confirm_icon: "delete-bin-6-line",
|
||||||
|
opt_out_id: "delete-item"
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
"""
|
||||||
|
def with_confirm(js \\ %JS{}, on_confirm, opts) do
|
||||||
|
opts =
|
||||||
|
Keyword.validate!(
|
||||||
|
opts,
|
||||||
|
[:confirm_icon, :description, :opt_out_id, title: "Are you sure?", confirm_text: "Yes"]
|
||||||
|
)
|
||||||
|
|
||||||
|
JS.dispatch(js, "lb:confirm_request",
|
||||||
|
detail: %{
|
||||||
|
on_confirm: Jason.encode!(on_confirm.ops),
|
||||||
|
title: opts[:title],
|
||||||
|
description: Keyword.fetch!(opts, :description),
|
||||||
|
confirm_text: opts[:confirm_text],
|
||||||
|
confirm_icon: opts[:confirm_icon],
|
||||||
|
opt_out_id: opts[:opt_out_id]
|
||||||
|
}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -20,6 +20,7 @@ defmodule LivebookWeb.HomeLive do
|
||||||
socket
|
socket
|
||||||
|> SidebarHelpers.shared_home_handlers()
|
|> SidebarHelpers.shared_home_handlers()
|
||||||
|> assign(
|
|> assign(
|
||||||
|
self_path: Routes.home_path(socket, :page),
|
||||||
file: determine_file(params),
|
file: determine_file(params),
|
||||||
file_info: %{exists: true, access: :read_write},
|
file_info: %{exists: true, access: :read_write},
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
|
@ -117,21 +118,21 @@ defmodule LivebookWeb.HomeLive do
|
||||||
|
|
||||||
<%= if @live_action == :user do %>
|
<%= if @live_action == :user do %>
|
||||||
<.current_user_modal
|
<.current_user_modal
|
||||||
return_to={Routes.home_path(@socket, :page)}
|
return_to={@self_path}
|
||||||
current_user={@current_user} />
|
current_user={@current_user} />
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :close_session do %>
|
<%= if @live_action == :close_session do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
|
<.modal id="close-session-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.HomeLive.CloseSessionComponent}
|
<.live_component module={LivebookWeb.HomeLive.CloseSessionComponent}
|
||||||
id="close-session"
|
id="close-session"
|
||||||
return_to={Routes.home_path(@socket, :page)}
|
return_to={@self_path}
|
||||||
session={@session} />
|
session={@session} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :import do %>
|
<%= if @live_action == :import do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
|
<.modal id="import-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.HomeLive.ImportComponent}
|
<.live_component module={LivebookWeb.HomeLive.ImportComponent}
|
||||||
id="import"
|
id="import"
|
||||||
tab={@tab}
|
tab={@tab}
|
||||||
|
@ -140,11 +141,11 @@ defmodule LivebookWeb.HomeLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :edit_sessions do %>
|
<%= if @live_action == :edit_sessions do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
|
<.modal id="edit-sessions-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.HomeLive.EditSessionsComponent}
|
<.live_component module={LivebookWeb.HomeLive.EditSessionsComponent}
|
||||||
id="edit-sessions"
|
id="edit-sessions"
|
||||||
action={@bulk_action}
|
action={@bulk_action}
|
||||||
return_to={Routes.home_path(@socket, :page)}
|
return_to={@self_path}
|
||||||
sessions={@sessions}
|
sessions={@sessions}
|
||||||
selected_sessions={selected_sessions(@sessions, @selected_session_ids)} />
|
selected_sessions={selected_sessions(@sessions, @selected_session_ids)} />
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
|
||||||
Close session
|
Close session
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-gray-700">
|
<p class="text-gray-700">
|
||||||
Are you sure you want to close this section -
|
Are you sure you want to close this session -
|
||||||
<span class="font-semibold">“<%= @session.notebook_name %>”</span>?
|
<span class="font-semibold">“<%= @session.notebook_name %>”</span>?
|
||||||
<br/>
|
<br/>
|
||||||
<%= if @session.file,
|
<%= if @session.file,
|
||||||
|
|
|
@ -47,6 +47,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(
|
|> assign(
|
||||||
|
self_path: Routes.session_path(socket, :page, session.id),
|
||||||
session: session,
|
session: session,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
self: self(),
|
self: self(),
|
||||||
|
@ -227,12 +228,12 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
<%= if @live_action == :user do %>
|
<%= if @live_action == :user do %>
|
||||||
<.current_user_modal
|
<.current_user_modal
|
||||||
return_to={Routes.session_path(@socket, :page, @session.id)}
|
return_to={@self_path}
|
||||||
current_user={@current_user} />
|
current_user={@current_user} />
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :runtime_settings do %>
|
<%= if @live_action == :runtime_settings do %>
|
||||||
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="runtime-settings-modal" show class="w-full max-w-4xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.RuntimeComponent}
|
<.live_component module={LivebookWeb.SessionLive.RuntimeComponent}
|
||||||
id="runtime-settings"
|
id="runtime-settings"
|
||||||
session={@session}
|
session={@session}
|
||||||
|
@ -241,7 +242,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :file_settings do %>
|
<%= if @live_action == :file_settings do %>
|
||||||
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="persistence-modal" show class="w-full max-w-4xl" patch={@self_path}>
|
||||||
<%= live_render @socket, LivebookWeb.SessionLive.PersistenceLive,
|
<%= live_render @socket, LivebookWeb.SessionLive.PersistenceLive,
|
||||||
id: "persistence",
|
id: "persistence",
|
||||||
session: %{
|
session: %{
|
||||||
|
@ -254,7 +255,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :shortcuts do %>
|
<%= if @live_action == :shortcuts do %>
|
||||||
<.modal class="w-full max-w-6xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="shortcuts-modal" show class="w-full max-w-6xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.ShortcutsComponent}
|
<.live_component module={LivebookWeb.SessionLive.ShortcutsComponent}
|
||||||
id="shortcuts"
|
id="shortcuts"
|
||||||
platform={@platform} />
|
platform={@platform} />
|
||||||
|
@ -262,49 +263,49 @@ defmodule LivebookWeb.SessionLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :cell_settings do %>
|
<%= if @live_action == :cell_settings do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="cell-settings-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={settings_component_for(@cell)}
|
<.live_component module={settings_component_for(@cell)}
|
||||||
id="cell-settings"
|
id="cell-settings"
|
||||||
session={@session}
|
session={@session}
|
||||||
return_to={Routes.session_path(@socket, :page, @session.id)}
|
return_to={@self_path}
|
||||||
cell={@cell} />
|
cell={@cell} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :cell_upload do %>
|
<%= if @live_action == :cell_upload do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="cell-upload-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.CellUploadComponent}
|
<.live_component module={LivebookWeb.SessionLive.CellUploadComponent}
|
||||||
id="cell-upload"
|
id="cell-upload"
|
||||||
session={@session}
|
session={@session}
|
||||||
return_to={Routes.session_path(@socket, :page, @session.id)}
|
return_to={@self_path}
|
||||||
cell={@cell}
|
cell={@cell}
|
||||||
uploads={@uploads} />
|
uploads={@uploads} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :delete_section do %>
|
<%= if @live_action == :delete_section do %>
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="delete-section-modal" show class="w-full max-w-xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.DeleteSectionComponent}
|
<.live_component module={LivebookWeb.SessionLive.DeleteSectionComponent}
|
||||||
id="delete-section"
|
id="delete-section"
|
||||||
session={@session}
|
session={@session}
|
||||||
return_to={Routes.session_path(@socket, :page, @session.id)}
|
return_to={@self_path}
|
||||||
section={@section}
|
section={@section}
|
||||||
is_first={@section.id == @first_section_id} />
|
is_first={@section.id == @first_section_id} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :bin do %>
|
<%= if @live_action == :bin do %>
|
||||||
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="bin-modal" show class="w-full max-w-4xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.BinComponent}
|
<.live_component module={LivebookWeb.SessionLive.BinComponent}
|
||||||
id="bin"
|
id="bin"
|
||||||
session={@session}
|
session={@session}
|
||||||
return_to={Routes.session_path(@socket, :page, @session.id)}
|
return_to={@self_path}
|
||||||
bin_entries={@data_view.bin_entries} />
|
bin_entries={@data_view.bin_entries} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :export do %>
|
<%= if @live_action == :export do %>
|
||||||
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
|
<.modal id="export-modal" show class="w-full max-w-4xl" patch={@self_path}>
|
||||||
<.live_component module={LivebookWeb.SessionLive.ExportComponent}
|
<.live_component module={LivebookWeb.SessionLive.ExportComponent}
|
||||||
id="export"
|
id="export"
|
||||||
session={@session}
|
session={@session}
|
||||||
|
|
|
@ -317,8 +317,16 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
<span class="tooltip top" data-tooltip="Delete">
|
<span class="tooltip top" data-tooltip="Delete">
|
||||||
<button class="icon-button"
|
<button class="icon-button"
|
||||||
aria-label="delete cell"
|
aria-label="delete cell"
|
||||||
phx-click="delete_cell"
|
phx-click={
|
||||||
phx-value-cell_id={@cell_id}>
|
with_confirm(
|
||||||
|
JS.push("delete_cell", value: %{cell_id: @cell_id}),
|
||||||
|
title: "Delete cell",
|
||||||
|
description: "Once you delete this cell, it will be moved to the bin.",
|
||||||
|
confirm_text: "Delete",
|
||||||
|
confirm_icon: "delete-bin-6-line",
|
||||||
|
opt_out_id: "delete-cell"
|
||||||
|
)
|
||||||
|
}>
|
||||||
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
|
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -122,22 +122,12 @@ defmodule LivebookWeb.SettingsLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :add_file_system do %>
|
<%= if @live_action == :add_file_system do %>
|
||||||
<.modal class="w-full max-w-3xl" return_to={Routes.settings_path(@socket, :page)}>
|
<.modal id="add-file-system-modal" show class="w-full max-w-3xl" patch={Routes.settings_path(@socket, :page)}>
|
||||||
<.live_component module={LivebookWeb.SettingsLive.AddFileSystemComponent}
|
<.live_component module={LivebookWeb.SettingsLive.AddFileSystemComponent}
|
||||||
id="add-file-system"
|
id="add-file-system"
|
||||||
return_to={Routes.settings_path(@socket, :page)} />
|
return_to={Routes.settings_path(@socket, :page)} />
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @live_action == :detach_file_system do %>
|
|
||||||
<.modal class="w-full max-w-xl" return_to={Routes.settings_path(@socket, :page)}>
|
|
||||||
<.live_component module={LivebookWeb.SettingsLive.RemoveFileSystemComponent}
|
|
||||||
id="detach-file-system"
|
|
||||||
return_to={Routes.settings_path(@socket, :page)}
|
|
||||||
file_system_id={@file_system_id}
|
|
||||||
/>
|
|
||||||
</.modal>
|
|
||||||
<% end %>
|
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,6 +138,13 @@ defmodule LivebookWeb.SettingsLive do
|
||||||
|
|
||||||
def handle_params(_params, _url, socket), do: {:noreply, socket}
|
def handle_params(_params, _url, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("detach_file_system", %{"id" => file_system_id}, socket) do
|
||||||
|
Livebook.Settings.remove_file_system(file_system_id)
|
||||||
|
file_systems = Livebook.Settings.file_systems()
|
||||||
|
{:noreply, assign(socket, file_systems: file_systems)}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:file_systems_updated, file_systems}, socket) do
|
def handle_info({:file_systems_updated, file_systems}, socket) do
|
||||||
{:noreply, assign(socket, file_systems: file_systems)}
|
{:noreply, assign(socket, file_systems: file_systems)}
|
||||||
|
|
|
@ -14,9 +14,18 @@ defmodule LivebookWeb.SettingsLive.FileSystemsComponent do
|
||||||
<.file_system_info file_system={file_system} />
|
<.file_system_info file_system={file_system} />
|
||||||
</div>
|
</div>
|
||||||
<%= unless is_struct(file_system, FileSystem.Local) do %>
|
<%= unless is_struct(file_system, FileSystem.Local) do %>
|
||||||
<%= live_patch "Detach",
|
<button class="button-base button-outlined-red"
|
||||||
to: Routes.settings_path(@socket, :detach_file_system, file_system_id),
|
phx-click={
|
||||||
class: "button-base button-outlined-red" %>
|
with_confirm(
|
||||||
|
JS.push("detach_file_system", value: %{id: file_system_id}),
|
||||||
|
title: "Detach file system",
|
||||||
|
description: "Are you sure you want to detach this file system? Any sessions using it will keep the access until they get closed.",
|
||||||
|
confirm_text: "Detach",
|
||||||
|
confirm_icon: "close-circle-line"
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Detach
|
||||||
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
defmodule LivebookWeb.SettingsLive.RemoveFileSystemComponent do
|
|
||||||
use LivebookWeb, :live_component
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="p-6 pb-4 flex flex-col space-y-8">
|
|
||||||
<h3 class="text-2xl font-semibold text-gray-800">
|
|
||||||
Detach file system
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-700">
|
|
||||||
Are you sure you want to detach this file system?
|
|
||||||
Any sessions using it will keep the access until
|
|
||||||
they get closed.
|
|
||||||
</p>
|
|
||||||
<div class="mt-8 flex justify-end space-x-2">
|
|
||||||
<button class="button-base button-red" phx-click="detach" phx-target={@myself}>
|
|
||||||
<.remix_icon icon="close-circle-line" class="align-middle mr-1" />
|
|
||||||
Detach
|
|
||||||
</button>
|
|
||||||
<%= live_patch "Cancel", to: @return_to, class: "button-base button-outlined-gray" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("detach", %{}, socket) do
|
|
||||||
Livebook.Settings.remove_file_system(socket.assigns.file_system_id)
|
|
||||||
send(self(), {:file_systems_updated, Livebook.Settings.file_systems()})
|
|
||||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,6 +4,7 @@ defmodule LivebookWeb.SidebarHelpers do
|
||||||
import LivebookWeb.Helpers
|
import LivebookWeb.Helpers
|
||||||
import LivebookWeb.UserHelpers
|
import LivebookWeb.UserHelpers
|
||||||
|
|
||||||
|
alias Phoenix.LiveView.JS
|
||||||
alias LivebookWeb.Router.Helpers, as: Routes
|
alias LivebookWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -60,9 +61,16 @@ defmodule LivebookWeb.SidebarHelpers do
|
||||||
<span class="tooltip right distant" data-tooltip="Shutdown">
|
<span class="tooltip right distant" data-tooltip="Shutdown">
|
||||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center"
|
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center"
|
||||||
aria-label="shutdown"
|
aria-label="shutdown"
|
||||||
phx-click="shutdown"
|
phx-click={
|
||||||
data-confirm="Are you sure you want to shutdown Livebook?">
|
with_confirm(
|
||||||
<.remix_icon icon="shut-down-line" />
|
JS.push("shutdown"),
|
||||||
|
title: "Shutdown",
|
||||||
|
description: "Are you sure you want to shutdown Livebook?",
|
||||||
|
confirm_text: "Shutdown",
|
||||||
|
confirm_icon: "shut-down-line"
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<.remix_icon icon="shut-down-line" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -47,7 +47,7 @@ defmodule LivebookWeb.UserHelpers do
|
||||||
"""
|
"""
|
||||||
def current_user_modal(assigns) do
|
def current_user_modal(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<LivebookWeb.Helpers.modal class="w-full max-w-sm" return_to={@return_to}>
|
<LivebookWeb.Helpers.modal id="user-modal" show class="w-full max-w-sm" patch={@return_to}>
|
||||||
<.live_component module={LivebookWeb.UserComponent}
|
<.live_component module={LivebookWeb.UserComponent}
|
||||||
id="user"
|
id="user"
|
||||||
return_to={@return_to}
|
return_to={@return_to}
|
||||||
|
|
|
@ -52,7 +52,6 @@ defmodule LivebookWeb.Router do
|
||||||
live "/settings", SettingsLive, :page
|
live "/settings", SettingsLive, :page
|
||||||
live "/settings/user-profile", SettingsLive, :user
|
live "/settings/user-profile", SettingsLive, :user
|
||||||
live "/settings/add-file-system", SettingsLive, :add_file_system
|
live "/settings/add-file-system", SettingsLive, :add_file_system
|
||||||
live "/settings/detach-file-system/:file_system_id", SettingsLive, :detach_file_system
|
|
||||||
|
|
||||||
live "/explore", ExploreLive, :page
|
live "/explore", ExploreLive, :page
|
||||||
live "/explore/user-profile", ExploreLive, :user
|
live "/explore/user-profile", ExploreLive, :user
|
||||||
|
|
|
@ -31,3 +31,5 @@
|
||||||
|
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<.confirm_modal />
|
||||||
|
|
Loading…
Add table
Reference in a new issue