add note references
This commit is contained in:
parent
cf8dd95064
commit
a62722fe17
6 changed files with 260 additions and 14 deletions
|
@ -5,10 +5,9 @@ defmodule Chiya.Notes do
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Chiya.Repo
|
alias Chiya.Repo
|
||||||
|
alias Chiya.Notes.{Note, NoteImage, NoteNote, References}
|
||||||
|
|
||||||
alias Chiya.Notes.{Note, NoteImage}
|
@preloads [:channels, :images, :links_from, :links_to]
|
||||||
|
|
||||||
@preloads [:channels, :images]
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of notes.
|
Returns the list of notes.
|
||||||
|
@ -61,6 +60,22 @@ defmodule Chiya.Notes do
|
||||||
"""
|
"""
|
||||||
def get_note!(id), do: Repo.get!(Note, id)
|
def get_note!(id), do: Repo.get!(Note, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single note.
|
||||||
|
|
||||||
|
Returns nil if the Note does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_note!(123)
|
||||||
|
%Note{}
|
||||||
|
|
||||||
|
iex> get_note!(456)
|
||||||
|
nil
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_note(id), do: Repo.get(Note, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single note and preloads it.
|
Gets a single note and preloads it.
|
||||||
|
|
||||||
|
@ -77,8 +92,38 @@ defmodule Chiya.Notes do
|
||||||
"""
|
"""
|
||||||
def get_note_preloaded!(id), do: Repo.get!(Note, id) |> preload_note()
|
def get_note_preloaded!(id), do: Repo.get!(Note, id) |> preload_note()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single note by its slug and preloads it.
|
||||||
|
|
||||||
|
Raises `Ecto.NoResultsError` if the Note does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_note_preloaded!(123)
|
||||||
|
%Note{}
|
||||||
|
|
||||||
|
iex> get_note_preloaded!(456)
|
||||||
|
** (Ecto.NoResultsError)
|
||||||
|
|
||||||
|
"""
|
||||||
def get_note_by_slug_preloaded!(slug), do: Repo.get_by!(Note, slug: slug) |> preload_note()
|
def get_note_by_slug_preloaded!(slug), do: Repo.get_by!(Note, slug: slug) |> preload_note()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets a single note by its slug and preloads it.
|
||||||
|
|
||||||
|
Returns nil if the Note does not exist.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> get_note_preloaded!(123)
|
||||||
|
%Note{}
|
||||||
|
|
||||||
|
iex> get_note_preloaded!(456)
|
||||||
|
nil
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_note_by_slug_preloaded(slug), do: Repo.get_by(Note, slug: slug) |> preload_note()
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a note.
|
Creates a note.
|
||||||
|
|
||||||
|
@ -113,6 +158,7 @@ defmodule Chiya.Notes do
|
||||||
note
|
note
|
||||||
|> Note.changeset(attrs)
|
|> Note.changeset(attrs)
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|> Chiya.Notes.References.update_references(attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -178,6 +224,24 @@ defmodule Chiya.Notes do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_note_note!(attrs \\ %{}) do
|
||||||
|
Repo.get_by!(NoteNote, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_note_note(attrs \\ %{}) do
|
||||||
|
Repo.get_by(NoteNote, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_note_note(attrs \\ %{}) do
|
||||||
|
%NoteNote{}
|
||||||
|
|> NoteNote.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_note_note(%NoteNote{} = note_note) do
|
||||||
|
Repo.delete(note_note)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking note_image changes.
|
Returns an `%Ecto.Changeset{}` for tracking note_image changes.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -19,6 +19,14 @@ defmodule Chiya.Notes.Note do
|
||||||
join_keys: [note: :id, channel: :id],
|
join_keys: [note: :id, channel: :id],
|
||||||
on_replace: :delete
|
on_replace: :delete
|
||||||
|
|
||||||
|
many_to_many :links_from, Chiya.Notes.Note,
|
||||||
|
join_through: Chiya.Notes.NoteNote,
|
||||||
|
join_keys: [target_id: :id, source_id: :id]
|
||||||
|
|
||||||
|
many_to_many :links_to, Chiya.Notes.Note,
|
||||||
|
join_through: Chiya.Notes.NoteNote,
|
||||||
|
join_keys: [source_id: :id, target_id: :id]
|
||||||
|
|
||||||
has_many :images, Chiya.Notes.NoteImage
|
has_many :images, Chiya.Notes.NoteImage
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
|
19
lib/chiya/notes/note_note.ex
Normal file
19
lib/chiya/notes/note_note.ex
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Chiya.Notes.NoteNote do
|
||||||
|
@moduledoc """
|
||||||
|
The NoteNote module
|
||||||
|
"""
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "notes_notes" do
|
||||||
|
belongs_to :source, Chiya.Notes.Note
|
||||||
|
belongs_to :target, Chiya.Notes.Note
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(note_note, attrs) do
|
||||||
|
note_note
|
||||||
|
|> cast(attrs, [:source_id, :target_id])
|
||||||
|
|> validate_required([:source_id, :target_id])
|
||||||
|
end
|
||||||
|
end
|
147
lib/chiya/notes/references.ex
Normal file
147
lib/chiya/notes/references.ex
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
defmodule Chiya.Notes.References do
|
||||||
|
@moduledoc """
|
||||||
|
Finds and replaces references to entities in a string.
|
||||||
|
|
||||||
|
A reference is an internal link like the following examples:
|
||||||
|
|
||||||
|
* `[[sustainablity]]` -> A note named *Sustainability*
|
||||||
|
* `[[tag:video-games]]` -> A tag named *Video Games*
|
||||||
|
* `[[list:blog]]` -> A list named *Blog*
|
||||||
|
* `[[a-long-unfitting-title|A simple title]]` -> A note named *A long unfitting title*
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
use Phoenix.VerifiedRoutes,
|
||||||
|
endpoint: ChiyaWeb.Endpoint,
|
||||||
|
router: ChiyaWeb.Router,
|
||||||
|
statics: ChiyaWeb.static_paths()
|
||||||
|
|
||||||
|
@reference_regex ~r/\[\[(?<id>[\d\w-]+)(?:\|(?<title>[\w\d\s'`äöüß]+))?\]\]/
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a list of references in `string`.
|
||||||
|
"""
|
||||||
|
def get_references(nil), do: []
|
||||||
|
def get_references(""), do: []
|
||||||
|
|
||||||
|
def get_references(string) do
|
||||||
|
@reference_regex
|
||||||
|
|> Regex.scan(string, capture: :all)
|
||||||
|
|> Enum.map(&map_to_tuple/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks each reference returned from `get_references/1` and validates its existence.
|
||||||
|
"""
|
||||||
|
def validate_references(references) do
|
||||||
|
Enum.map(references, fn {placeholder, type, slug, custom_title} ->
|
||||||
|
{note, valid} =
|
||||||
|
case Chiya.Notes.get_note_by_slug_preloaded(slug) do
|
||||||
|
nil -> {nil, false}
|
||||||
|
note -> {note, true}
|
||||||
|
end
|
||||||
|
|
||||||
|
# If a custom title was defined, use it,
|
||||||
|
# otherwise use the notes title
|
||||||
|
title =
|
||||||
|
if(slug == custom_title,
|
||||||
|
do: note.name,
|
||||||
|
else: custom_title
|
||||||
|
)
|
||||||
|
|
||||||
|
{placeholder, type, slug, title, valid}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a list of slugs that are referenced in `string`, optionally filtering by `filter_type`.
|
||||||
|
"""
|
||||||
|
def get_reference_ids(string) do
|
||||||
|
string
|
||||||
|
|> get_references()
|
||||||
|
|> Enum.map(fn {_, note_slug, _} = _ref -> note_slug end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Finds and replaces references with the matching url in `string`.
|
||||||
|
"""
|
||||||
|
def replace_references(string) do
|
||||||
|
string
|
||||||
|
|> get_references()
|
||||||
|
|> validate_references()
|
||||||
|
|> Enum.reduce(string, fn {placeholder, slug, title, valid}, s ->
|
||||||
|
String.replace(s, placeholder, get_link(slug, title, valid))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def update_references({:ok, note}, attrs) do
|
||||||
|
new_reference_slugs = get_reference_ids(attrs["content"])
|
||||||
|
old_reference_slugs = Enum.map(note.links_from, fn n -> n.slug end)
|
||||||
|
references_to_add = new_reference_slugs -- old_reference_slugs
|
||||||
|
references_to_remove = old_reference_slugs -- new_reference_slugs
|
||||||
|
|
||||||
|
add_note_links(note, references_to_add)
|
||||||
|
remove_note_links(note, references_to_remove)
|
||||||
|
|
||||||
|
{:ok, note}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_references(error, _attrs), do: error
|
||||||
|
|
||||||
|
defp add_note_links(origin_note, slugs) do
|
||||||
|
Enum.each(slugs, fn slug ->
|
||||||
|
case linked_note = Chiya.Notes.get_note_by_slug_preloaded(slug) do
|
||||||
|
nil ->
|
||||||
|
Logger.warn("Reference to '#{slug}' could not be resolved")
|
||||||
|
|
||||||
|
_note ->
|
||||||
|
add_note_link(slug, origin_note, linked_note)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_note_link(slug, origin_note, linked_note) do
|
||||||
|
with attrs <- get_attrs(origin_note.id, linked_note.id),
|
||||||
|
{:ok, _note_note} <- Chiya.Notes.create_note_note(attrs) do
|
||||||
|
Logger.info("Reference to '#{slug}' created")
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
Logger.warn(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_note_links(origin_note, slugs) do
|
||||||
|
Enum.each(slugs, fn slug ->
|
||||||
|
linked_note = Chiya.Notes.get_note_by_slug_preloaded!(slug)
|
||||||
|
attrs = get_attrs(origin_note.id, linked_note.id)
|
||||||
|
note_note = Chiya.Notes.get_note_note(attrs)
|
||||||
|
|
||||||
|
case Chiya.Notes.delete_note_note(note_note) do
|
||||||
|
{:ok, _note_note} ->
|
||||||
|
Logger.info("Reference to '#{slug}' deleted")
|
||||||
|
|
||||||
|
error ->
|
||||||
|
Logger.warn(error)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_attrs(origin_id, linked_id),
|
||||||
|
do: %{
|
||||||
|
source_id: origin_id,
|
||||||
|
target_id: linked_id
|
||||||
|
}
|
||||||
|
|
||||||
|
defp get_link(slug, title, valid), do: "[#{title}](#{~p"/#{slug}"})#{get_link_class(valid)}"
|
||||||
|
|
||||||
|
defp get_link_class(false), do: "{:.invalid}"
|
||||||
|
defp get_link_class(_), do: ""
|
||||||
|
|
||||||
|
defp map_to_tuple([placeholder, note_slug]),
|
||||||
|
do: {placeholder, note_slug, note_slug}
|
||||||
|
|
||||||
|
defp map_to_tuple([placeholder, note_slug, title]),
|
||||||
|
do: {placeholder, note_slug, title}
|
||||||
|
end
|
|
@ -15,7 +15,7 @@ defmodule ChiyaWeb.NoteShowLive do
|
||||||
<:subtitle><%= @note.slug %></:subtitle>
|
<:subtitle><%= @note.slug %></:subtitle>
|
||||||
<:actions>
|
<:actions>
|
||||||
<.link href={~p"/admin/notes/#{@note}/edit"}>
|
<.link href={~p"/admin/notes/#{@note}/edit"}>
|
||||||
<.button>Edit note</.button>
|
<.button>Edit</.button>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={~p"/#{@note.slug}"}>
|
<.link href={~p"/#{@note.slug}"}>
|
||||||
<.button>Preview</.button>
|
<.button>Preview</.button>
|
||||||
|
@ -31,18 +31,10 @@ defmodule ChiyaWeb.NoteShowLive do
|
||||||
<:item title="Channels"><%= @channels %></:item>
|
<:item title="Channels"><%= @channels %></:item>
|
||||||
<:item title="Kind"><%= @note.kind %></:item>
|
<:item title="Kind"><%= @note.kind %></:item>
|
||||||
<:item title="Url"><%= @note.url %></:item>
|
<:item title="Url"><%= @note.url %></:item>
|
||||||
|
<:item title="Links outgoing"><%= note_links(@note.links_to) %></:item>
|
||||||
|
<:item title="Links incoming"><%= note_links(@note.links_from) %></:item>
|
||||||
</.list>
|
</.list>
|
||||||
|
|
||||||
<.line />
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary class="text-gray-900 dark:text-gray-100">File Content</summary>
|
|
||||||
<section class="prose">
|
|
||||||
<%= raw ChiyaWeb.Markdown.render(@note.content) %>
|
|
||||||
</section>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
<.line />
|
<.line />
|
||||||
|
|
||||||
<%= if !Enum.empty?(@note.images) do %>
|
<%= if !Enum.empty?(@note.images) do %>
|
||||||
|
@ -158,4 +150,8 @@ defmodule ChiyaWeb.NoteShowLive do
|
||||||
|
|
||||||
{:noreply, assign(socket, :note, Notes.get_note_preloaded!(socket.assigns.note.id))}
|
{:noreply, assign(socket, :note, Notes.get_note_preloaded!(socket.assigns.note.id))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp note_links(notes) do
|
||||||
|
Enum.map_join(notes, ", ",fn n -> n.name end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule Chiya.Repo.Migrations.AddNotesNotesTable do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:notes_notes) do
|
||||||
|
add :source_id, references(:notes, on_delete: :delete_all)
|
||||||
|
add :target_id, references(:notes, on_delete: :delete_all)
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:notes_notes, [:source_id, :target_id])
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue