Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Spike sidebar in liveview #294

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions assets/svelte/components/GoToParentButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import { setSelection, selectedAstElementId } from "$lib/stores/page"
import { getParentNodeId } from "$lib/utils/ast-helpers"

function selectParentNode() {
let parentId = getParentNodeId($selectedAstElementId)
setSelection(parentId)
}
</script>


<button type="button" class="absolute p-2 top-2 right-9 group" on:click={selectParentNode}>
<span class="sr-only">Up one level</span>
<span
class="absolute opacity-0 invisible right-9 min-w-[100px] bg-amber-100 py-1 px-1.5 rounded text-xs text-medium transition group-hover:opacity-100 group-hover:visible"
>Up one level</span
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 hover:text-blue-700 active:text-blue-900"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12"
/>
</svg>
</button>
1 change: 0 additions & 1 deletion assets/svelte/components/PropertiesSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
page,
selectedAstElement,
selectedAstElementId,
findAstElement,
isAstElement,
setSelection,
resetSelection,
Expand Down
6 changes: 2 additions & 4 deletions assets/svelte/components/UiBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
export let tailwindConfig: string
export let tailwindInput: string
export let live

$: $pageStore = page
$: $tailwindConfigStore = tailwindConfig
$: $tailwindInputStore = tailwindInput
Expand All @@ -30,16 +31,13 @@
}
</script>

<Backdrop />
<div class="flex min-h-screen bg-gray-100" id="ui-builder-app-container" data-testid="app-container">
<Backdrop />
<!-- Left sidebar -->
<ComponentsSidebar {components} />

<!-- Main -->
<PagePreview />

<!-- Right sidebar -->
<PropertiesSidebar on:droppedIntoTarget={(e) => addBasicComponentToTarget(e.detail)} />

<SelectedElementFloatingMenu />
</div>
5 changes: 4 additions & 1 deletion assets/svelte/stores/page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { writable, derived, get } from "svelte/store"
import type { Writable, Readable } from "svelte/store"
import type { AstElement, AstNode, Page } from "$lib/types"
import { live } from "$lib/stores/live"

export const page: Writable<Page> = writable()
export const selectedAstElementId: Writable<string | undefined> = writable()
Expand All @@ -17,7 +18,9 @@ export const selectedAstElement: Readable<AstElement | undefined> = derived(
[page, selectedAstElementId],
([$page, $selectedAstElementId]) => {
if ($page && $selectedAstElementId) {
return findAstElement($page.ast, $selectedAstElementId)
const element = findAstElement($page.ast, $selectedAstElementId)
get(live).pushEvent('select_ast_element', { id: $selectedAstElementId })
return element
}
},
)
Expand Down
150 changes: 150 additions & 0 deletions lib/beacon/live_admin/components/properties_sidebar_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
defmodule Beacon.LiveAdmin.PropertiesSidebarComponent do

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function update_many/1 in module Beacon.LiveAdmin.PropertiesSidebarComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function update/2 in module Beacon.LiveAdmin.PropertiesSidebarComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function render/1 in module Beacon.LiveAdmin.PropertiesSidebarComponent)
# In Phoenix apps, the line is typically: use MyAppWeb, :live_component
use Phoenix.LiveComponent
use Beacon.LiveAdmin.Web, :live_component
alias Beacon.LiveAdmin.PropertiesSidebarSectionComponent
require Logger

defmodule Attribute do
use Ecto.Schema
import Ecto.Changeset

# Define an embedded schema (no database backing)
embedded_schema do
field :name, :string
field :value, :string
end

# Function to create and validate changeset
def changeset(attrs) do
%__MODULE__{}
|> cast(attrs, [:name, :value])
|> validate_required([:name, :value])
end
end


def mount(socket) do
socket = assign(socket, :new_attributes, [])
{:ok, socket}
end

def update(assigns, socket) do
selected_ast_element = case assigns.selected_ast_element_id do
"root" -> %{ "tag" => "root", "attrs" => %{}, "content" => assigns.page.ast }
xpath -> find_ast_element(assigns.page.ast, xpath)
end

socket =
assign(socket,
selected_ast_element: selected_ast_element,
attributes_editable: selected_ast_element["tag"] not in ["eex", "eex_block"]
)

{:ok, assign(socket, assigns)}
end

def find_ast_element(_nodes, nil), do: nil
def find_ast_element(nodes, xpath) do
parts = String.split(xpath, ".") |> Enum.map(&String.to_integer/1)
find_ast_element_recursive(nodes, parts)
end

defp find_ast_element_recursive(nodes, [index | []]), do: Enum.at(nodes, index)
defp find_ast_element_recursive(nodes, [index | rest]) do
case Enum.at(nodes, index) do
nil -> nil
node -> find_ast_element_recursive(node["content"], rest)
end
end

@spec handle_event(<<_::104, _::_*24>>, any(), %{
:assigns => atom() | %{:new_attributes => list(), optional(any()) => any()},
optional(any()) => any()
}) :: {:noreply, map()}
def handle_event("add_attribute", _params, socket) do
new_attribute = Attribute.changeset(%{"name" => "", "value" => ""})
new_attributes = socket.assigns.new_attributes ++ [new_attribute]
Logger.debug("############################## Adding new attribute")
Logger.debug("############################## New attributes: #{inspect(new_attributes)}")
{:noreply, assign(socket, :new_attributes, new_attributes)}
end

def handle_event("delete_attribute", %{"index" => index}, socket) do
new_attributes = List.delete_at(socket.assigns.new_attributes, String.to_integer(index))
{:noreply, assign(socket, :new_attributes, new_attributes)}
end

@spec render(
atom()
| %{:page => atom() | %{:ast => any(), optional(any()) => any()}, :selected_ast_element_id => nil | binary(), optional(any()) => any()}
) :: Phoenix.LiveView.Rendered.t()
def render(assigns) do
~H"""
<div class="mt-4 w-64 bg-white" data-testid="right-sidebar">
<div class="sticky top-0 overflow-y-auto h-screen">
<%= if @selected_ast_element do %>
<div class="border-b text-lg font-medium leading-5 p-4 relative">
<%= @selected_ast_element["tag"] %>
<.go_to_parent_button selected_ast_element_id={assigns.selected_ast_element_id} socket={@socket}/>
<.close_button />
</div>

<%= if @attributes_editable do %>
<%!-- Editable attributes --%>
<%!-- <%= for {{name, value}, index} <- Enum.with_index(@selected_ast_element["attrs"]) do %>
<.live_component module={PropertiesSidebarSectionComponent} id="class-section" attribute_changeset={changeset} parent={@myself} edit_name={false} index={index} />
<% end %> --%>

<%!-- New attributes --%>
<%= for {changeset, index} <- Enum.with_index(@new_attributes) do %>
<.live_component module={PropertiesSidebarSectionComponent} id={"new-attribute-section-#{index}"} parent={@myself} attribute_changeset={changeset} edit_name={true} index={index} />
<% end %>
<% end %>
<div class="p-4">
<.add_attribute_button parent={@myself}/>
</div>
<% end %>
</div>
</div>
"""
end

def close_button(assigns) do
~H"""
<button type="button" class="absolute p-2 top-2 right-1" phx-click="reset_selection">
<span class="sr-only">Close</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6 hover:text-blue-700 active:text-blue-900"
>
<path
fill-rule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z"
clip-rule="evenodd"
/>
</svg>
</button>
"""
end

def go_to_parent_button(assigns) do
~H"""
<%= if @selected_ast_element_id !== "root" do %>
<.svelte name="components/GoToParentButton" class="contents" socket={@socket}/>
<% end %>
"""
end

def add_attribute_button(assigns) do
~H"""
<button
type="button"
class="bg-blue-500 hover:bg-blue-700 active:bg-blue-800 text-white font-bold py-2 px-4 rounded outline-2 w-full"
phx-click="add_attribute"
phx-target={@parent}>+ Add attribute</button>
"""
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule Beacon.LiveAdmin.PropertiesSidebarSectionComponent do

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function update_many/1 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function update/2 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function render/1 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function mount/1 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function handle_event/3 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)

Check warning on line 1 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

the behavior Phoenix.LiveComponent has been declared twice (conflict in function handle_async/3 in module Beacon.LiveAdmin.PropertiesSidebarSectionComponent)
# In Phoenix apps, the line is typically: use MyAppWeb, :live_component
use Phoenix.LiveComponent
use Beacon.LiveAdmin.Web, :live_component

Check warning on line 4 in lib/beacon/live_admin/components/properties_sidebar_section_component.ex

View workflow job for this annotation

GitHub Actions / test: OTP 25.1 | Elixir 1.14.0 | Phoenix 1.7.0 | LV 0.20.2 | PG 13.0-alpine

this clause for __live__/0 cannot match because a previous clause at line 3 always matches
require Logger

def update(assigns, socket) do
Logger.debug("########## PropertiesSidebarSectionComponent update assigns: #{inspect(assigns)}")
{:ok,
assign(socket, assigns) |> assign(:form, to_form(assigns.attribute_changeset))
}
end

def render(assigns) do
~H"""
<section class="p-4 border-b border-b-gray-100 border-solid">
<.form for={@form} phx-submit="check_and_save">
<header class="flex items-center text-sm mb-2 font-medium">
<div class="w-full flex items-center justify-between gap-x-1 p-1 font-semibold group">
<span class="flex-grow">
<span class="hover:text-blue-700 active:text-blue-900">
<%= if @edit_name do %>
<.input field={@form[:name]} type="text" class="w-full py-1 px-2 bg-gray-100 border-gray-100 rounded-md leading-6 text-sm"/>
<% else %>
<%= @attribute_changeset[:name] %>
<% end %>
</span>
</span>
<.delete_button index={@index} parent={@parent}/>
<.toggle_button/>
</div>
</header>
<.input field={@form[:value]} type="text" class="w-full py-1 px-2 bg-gray-100 border-gray-100 rounded-md leading-6 text-sm" />
</.form>
</section>
"""
end

def delete_button(assigns) do
~H"""
<button type="button" class="ml-4" title="Delete attribute" phx-click="delete_attribute" phx-value-index={@index} phx-target={@parent}>
<span class="hero-trash text-red hover:text-red"></span>
</button>
"""
end

def toggle_button(assigns) do
~H"""
<button type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 stroke-slate-500 fill-slate-500 group-hover:stroke-current group-hover:fill-current"
>
<path
fill-rule="evenodd"
d="M11.47 7.72a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 1 1-1.06 1.06L12 9.31l-6.97 6.97a.75.75 0 0 1-1.06-1.06l7.5-7.5Z"
clip-rule="evenodd"
/>
</svg>
</button>
"""
end
end
11 changes: 10 additions & 1 deletion lib/beacon/live_admin/live/page_editor_live/edit.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Beacon.LiveAdmin.PageEditorLive.Edit do
@moduledoc false

require Logger
use Beacon.LiveAdmin.PageBuilder
alias Beacon.LiveAdmin.Content
alias Beacon.LiveAdmin.WebAPI
Expand All @@ -16,6 +16,7 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Edit do

socket =
socket
|> assign_new(:selected_ast_element_id, fn -> nil end)
|> assign_new(:layouts, fn -> Content.list_layouts(site) end)
|> assign_new(:components, fn ->
components = Content.list_components(site, per_page: :infinity)
Expand Down Expand Up @@ -79,6 +80,13 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Edit do
{:noreply, socket}
end

def handle_event("select_ast_element", %{ "id" => id }, socket) do
{:noreply,
socket
|> assign(selected_ast_element_id: id)
}
end

@impl true
def render(assigns) do
~H"""
Expand All @@ -90,6 +98,7 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Edit do
site={@beacon_page.site}
layouts={@layouts}
page={@page}
selected_ast_element_id={@selected_ast_element_id}
components={@components}
editor={@editor}
patch="/pages"
Expand Down
35 changes: 21 additions & 14 deletions lib/beacon/live_admin/live/page_editor_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Beacon.LiveAdmin.PageEditorLive.FormComponent do
alias Beacon.LiveAdmin.Content
alias Beacon.LiveAdmin.RuntimeCSS
alias Beacon.LiveAdmin.WebAPI
alias Beacon.LiveAdmin.PropertiesSidebarComponent
alias Ecto.Changeset

@impl true
Expand Down Expand Up @@ -285,20 +286,26 @@ defmodule Beacon.LiveAdmin.PageEditorLive.FormComponent do
</div>
</.modal>

<.svelte
:if={@editor == "visual"}
name="components/UiBuilder"
class={svelte_page_builder_class(@editor)}
props={
%{
components: @components,
page: @builder_page,
tailwindConfig: @tailwind_config,
tailwindInput: @tailwind_input
}
}
socket={@socket}
/>
<%= if @editor == "visual" do %>
<div class="flex">
<.svelte
name="components/UiBuilder"
class={svelte_page_builder_class(@editor)}
props={
%{
components: @components,
page: @builder_page,
tailwindConfig: @tailwind_config,
tailwindInput: @tailwind_input,
selectedAstElementId: @selected_ast_element_id
}
}
socket={@socket}
/>
<.live_component module={PropertiesSidebarComponent} id="properties_sidebar" page={@builder_page} selected_ast_element_id={@selected_ast_element_id} />
</div>
<% end %>


<div class={[
"grid items-start grid-cols-1 mx-auto mt-4 gap-x-8 gap-y-8 lg:mx-0 lg:max-w-none lg:grid-cols-3 h-auto",
Expand Down
Loading