diff --git a/assets/css/app.css b/assets/css/app.css
index 091a0db..8c71781 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -84,7 +84,7 @@
/* Set width and color for identity icons */
a[rel] svg {
width: 1em;
- fill: rgb(var(--color-text-primary));
+ fill: rgb(var(--color-primary));
}
/* Set width and color for identity icons */
diff --git a/lib/chiya/notes.ex b/lib/chiya/notes.ex
index bcd3b6e..124ed4c 100644
--- a/lib/chiya/notes.ex
+++ b/lib/chiya/notes.ex
@@ -5,9 +5,9 @@ defmodule Chiya.Notes do
import Ecto.Query, warn: false
alias Chiya.Repo
- alias Chiya.Notes.{Note, NoteImage, NoteNote, References}
+ alias Chiya.Notes.{Note, NoteImage, NoteNote, NoteTag}
- @preloads [:channels, :images, :links_from, :links_to]
+ @preloads [:channels, :images, :links_from, :links_to, :tags]
@doc """
Returns the list of notes.
@@ -158,6 +158,7 @@ defmodule Chiya.Notes do
note
|> Note.changeset(attrs)
|> Repo.update()
+ |> Chiya.Tags.TagUpdater.update_tags(attrs)
|> Chiya.Notes.References.update_references(attrs)
end
@@ -248,4 +249,18 @@ defmodule Chiya.Notes do
def change_note_image(%NoteImage{} = note_image, attrs \\ %{}) do
NoteImage.update_changeset(note_image, attrs)
end
+
+ def get_note_tag!(attrs \\ %{}) do
+ Repo.get_by!(NoteTag, attrs)
+ end
+
+ def create_note_tag(attrs \\ %{}) do
+ %NoteTag{}
+ |> NoteTag.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ def delete_note_tag(%NoteTag{} = note_tag) do
+ Repo.delete(note_tag)
+ end
end
diff --git a/lib/chiya/notes/note.ex b/lib/chiya/notes/note.ex
index b5f9dbe..6ce8c28 100644
--- a/lib/chiya/notes/note.ex
+++ b/lib/chiya/notes/note.ex
@@ -8,7 +8,11 @@ defmodule Chiya.Notes.Note do
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :channels]}
schema "notes" do
field :content, :string
- field :kind, Ecto.Enum, values: [:post, :bookmark], default: :post
+
+ field :kind, Ecto.Enum,
+ values: [:post, :bookmark, :recipe],
+ default: :post
+
field :name, :string
field :published_at, :naive_datetime
field :slug, NoteSlug.Type
@@ -27,8 +31,16 @@ defmodule Chiya.Notes.Note do
join_through: Chiya.Notes.NoteNote,
join_keys: [source_id: :id, target_id: :id]
+ many_to_many :tags, Chiya.Tags.Tag,
+ join_through: Chiya.Notes.NoteTag,
+ join_keys: [note_id: :id, tag_id: :id]
+
has_many :images, Chiya.Notes.NoteImage
+ field :tags_string, :string,
+ virtual: true,
+ default: ""
+
timestamps()
end
diff --git a/lib/chiya/notes/note_import.ex b/lib/chiya/notes/note_import.ex
index 66627b0..7204fa2 100644
--- a/lib/chiya/notes/note_import.ex
+++ b/lib/chiya/notes/note_import.ex
@@ -1,13 +1,13 @@
defmodule Chiya.Notes.NoteImport do
- import Ecto.Changeset
+ import Ecto.Changeset
- defstruct [:file]
+ defstruct [:file]
- @types %{file: :string}
+ @types %{file: :string}
- def change_note_import(params) do
- {%Chiya.Notes.NoteImport{}, @types}
- |> cast(params, Map.keys(@types))
- |> validate_required(:file)
- end
-end
\ No newline at end of file
+ def change_note_import(params) do
+ {%Chiya.Notes.NoteImport{}, @types}
+ |> cast(params, Map.keys(@types))
+ |> validate_required(:file)
+ end
+end
diff --git a/lib/chiya/notes/note_tag.ex b/lib/chiya/notes/note_tag.ex
new file mode 100644
index 0000000..fe27e3e
--- /dev/null
+++ b/lib/chiya/notes/note_tag.ex
@@ -0,0 +1,19 @@
+defmodule Chiya.Notes.NoteTag do
+ @moduledoc """
+ The NoteTag module
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "notes_tags" do
+ belongs_to :note, Chiya.Notes.Note
+ belongs_to :tag, Chiya.Tags.Tag
+ end
+
+ @doc false
+ def changeset(note_tag, attrs) do
+ note_tag
+ |> cast(attrs, [:note_id, :tag_id])
+ |> validate_required([:note_id, :tag_id])
+ end
+end
diff --git a/lib/chiya/notes/references.ex b/lib/chiya/notes/references.ex
index 2a24e37..b0b686b 100644
--- a/lib/chiya/notes/references.ex
+++ b/lib/chiya/notes/references.ex
@@ -75,7 +75,6 @@ defmodule Chiya.Notes.References do
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)
@@ -118,13 +117,18 @@ defmodule Chiya.Notes.References do
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")
+ if note_note do
+ case Chiya.Notes.delete_note_note(note_note) do
+ {:ok, _note_note} ->
+ Logger.info("Reference to '#{slug}' deleted")
- error ->
- Logger.warn(error)
+ error ->
+ Logger.warn(error)
+ end
+ else
+ Logger.debug("Note '#{slug}' does not exist anymore.")
end
+
end)
end
diff --git a/lib/chiya/tags.ex b/lib/chiya/tags.ex
new file mode 100644
index 0000000..1e69b28
--- /dev/null
+++ b/lib/chiya/tags.ex
@@ -0,0 +1,136 @@
+defmodule Chiya.Tags do
+ @moduledoc """
+ The Tags context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Chiya.Repo
+
+ alias Chiya.Tags.Tag
+
+ @preloads [:notes]
+ defp with_preloads(query), do: preload(query, ^@preloads)
+
+ @doc """
+ Preloads a tag with all defined preloads.
+
+ ## Examples
+
+ iex> preload_tag(tag)
+ %Tag{}
+
+ """
+ def preload_tag(tag), do: Repo.preload(tag, @preloads)
+
+ @doc """
+ Returns the list of tags.
+
+ ## Examples
+
+ iex> list_tags()
+ [%Tag{}, ...]
+
+ """
+ def list_tags do
+ Tag
+ |> with_preloads()
+ |> order_by(:title)
+ |> Repo.all()
+ end
+
+ @doc """
+ Gets a single tag.
+
+ Raises `Ecto.NoResultsError` if the Tag does not exist.
+
+ ## Examples
+
+ iex> get_tag!(123)
+ %Tag{}
+
+ iex> get_tag!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_tag_by_slug!(slug), do: Repo.get_by!(Tag, slug: slug) |> preload_tag()
+
+ @doc """
+ Gets a single tag.
+
+ Returns nil if the Tag does not exist.
+
+ ## Examples
+
+ iex> get_tag(123)
+ %Tag{}
+
+ iex> get_tag(456)
+ nil
+ """
+ def get_tag_by_slug(slug), do: Repo.get_by(Tag, slug: slug) |> preload_tag()
+
+ @doc """
+ Creates a tag.
+
+ ## Examples
+
+ iex> create_tag(%{field: value})
+ {:ok, %Tag{}}
+
+ iex> create_tag(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_tag(attrs \\ %{}) do
+ %Tag{}
+ |> Tag.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a tag.
+
+ ## Examples
+
+ iex> update_tag(tag, %{field: new_value})
+ {:ok, %Tag{}}
+
+ iex> update_tag(tag, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_tag(%Tag{} = tag, attrs) do
+ tag
+ |> Tag.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a tag.
+
+ ## Examples
+
+ iex> delete_tag(tag)
+ {:ok, %Tag{}}
+
+ iex> delete_tag(tag)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_tag(%Tag{} = tag) do
+ Repo.delete(tag)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking tag changes.
+
+ ## Examples
+
+ iex> change_tag(tag)
+ %Ecto.Changeset{data: %Tag{}}
+
+ """
+ def change_tag(%Tag{} = tag, attrs \\ %{}) do
+ Tag.changeset(tag, attrs)
+ end
+end
diff --git a/lib/chiya/tags/tag.ex b/lib/chiya/tags/tag.ex
new file mode 100644
index 0000000..90952a8
--- /dev/null
+++ b/lib/chiya/tags/tag.ex
@@ -0,0 +1,32 @@
+defmodule Chiya.Tags.Tag do
+ @moduledoc """
+ The Tag Schema
+ """
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias Chiya.Tags.TagSlug
+
+ schema "tags" do
+ field :name, :string
+ field :slug, TagSlug.Type
+
+ field :content, :string
+
+ field :icon, :string
+ field :regex, :string
+
+ many_to_many :notes, Chiya.Notes.Note, join_through: "notes_tags"
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(tag, attrs) do
+ tag
+ |> cast(attrs, [:name, :content, :icon, :regex])
+ |> validate_required([:name])
+ |> unique_constraint(:name)
+ |> TagSlug.maybe_generate_slug()
+ |> TagSlug.unique_constraint()
+ end
+end
diff --git a/lib/chiya/tags/tag_slug.ex b/lib/chiya/tags/tag_slug.ex
new file mode 100644
index 0000000..36b49bf
--- /dev/null
+++ b/lib/chiya/tags/tag_slug.ex
@@ -0,0 +1,3 @@
+defmodule Chiya.Tags.TagSlug do
+ use EctoAutoslugField.Slug, from: :name, to: :slug
+end
diff --git a/lib/chiya/tags/tag_updater.ex b/lib/chiya/tags/tag_updater.ex
new file mode 100644
index 0000000..6c6d21c
--- /dev/null
+++ b/lib/chiya/tags/tag_updater.ex
@@ -0,0 +1,112 @@
+defmodule Chiya.Tags.TagUpdater do
+ @moduledoc """
+ Updates tags for a schema like notes by adding new ones and removing old ones.
+ """
+
+ require Logger
+
+ alias Chiya.{Notes, Tags}
+ alias Chiya.Notes.Note
+
+ def update_tags({:ok, %Note{} = note}, attrs) do
+ update_tags(note, attrs)
+
+ {:ok, note}
+ end
+
+ @doc """
+ Updates the tags for the given schema
+
+ ## Examples
+
+ iex> update_tags(note, "foo,bar")
+
+ """
+ def update_tags(schema, %{tags_string: new_tags} = attrs) when is_map(attrs) do
+ update_tags(schema, new_tags)
+ end
+
+ def update_tags(schema, %{"tags_string" => new_tags} = attrs) when is_map(attrs) do
+ update_tags(schema, new_tags)
+ end
+
+ def update_tags(schema, attrs) when is_map(attrs) do
+ schema
+ end
+
+ def update_tags(schema, new_tags) when is_binary(new_tags) do
+ update_tags(schema, split_tags(new_tags))
+ end
+
+ def update_tags(%{tags: tags} = schema, new_tags) when is_list(new_tags) do
+ old_tags = Enum.map(tags, fn tag -> tag.name end)
+
+ Logger.info("Adding tags #{inspect(new_tags -- old_tags)}")
+ Logger.info("Removing tags #{inspect(old_tags -- new_tags)}")
+
+ schema
+ |> add_tags(new_tags -- old_tags)
+ |> remove_tags(old_tags -- new_tags)
+ end
+
+ defp split_tags(tags_string) when is_binary(tags_string) do
+ tags_string
+ |> String.split(",")
+ |> Enum.map(&String.trim/1)
+ |> Enum.filter(&(String.length(&1) > 0))
+ end
+
+ defp add_tags(schema, tags) do
+ Enum.each(tags, &add_tag(schema, &1))
+ schema
+ end
+
+ defp add_tag(%{id: schema_id} = schema, tag) when is_binary(tag) do
+ slug = Slugger.slugify_downcase(tag)
+
+ Logger.debug("Looking up tag [#{tag}] with slug [#{slug}]")
+
+ {:ok, tag} =
+ case Tags.get_tag(slug) do
+ nil ->
+ Logger.debug("Tag [#{tag}] does not exist. Creating.")
+ Tags.create_tag(%{name: tag})
+
+ tag ->
+ Logger.debug("Tag already exists. Returning.")
+ {:ok, tag}
+ end
+
+ case schema do
+ %Note{} ->
+ attrs = %{
+ note_id: schema_id,
+ tag_id: tag.id
+ }
+
+ {:ok, _note_tag} = Notes.create_note_tag(attrs)
+ end
+ end
+
+ defp remove_tags(schema, tags) do
+ Enum.each(tags, &remove_tag(schema, &1))
+ schema
+ end
+
+ defp remove_tag(schema, tag) do
+ slug = Slugger.slugify_downcase(tag)
+
+ if tag = Tags.get_tag(slug) do
+ case schema do
+ %Note{} ->
+ attrs = %{tag_id: tag.id, note_id: schema.id}
+ note_tag = Notes.get_note_tag!(attrs)
+
+ {:ok, _} = Notes.delete_note_tag(note_tag)
+ end
+ else
+ Logger.warn("Tag with slug #{slug} was not removed.")
+ nil
+ end
+ end
+end
diff --git a/lib/chiya_web/components/core_components.ex b/lib/chiya_web/components/core_components.ex
index 8437182..0a0400f 100644
--- a/lib/chiya_web/components/core_components.ex
+++ b/lib/chiya_web/components/core_components.ex
@@ -321,8 +321,11 @@ defmodule ChiyaWeb.CoreComponents do
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
- attr :rest, :global, include: ~w(autocomplete accept cols disabled form max maxlength min minlength
+
+ attr :rest, :global,
+ include: ~w(autocomplete accept cols disabled form max maxlength min minlength
pattern placeholder readonly required rows size step)
+
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
diff --git a/lib/chiya_web/components/layouts/public.html.heex b/lib/chiya_web/components/layouts/public.html.heex
index 4cbcab6..ae5dcab 100644
--- a/lib/chiya_web/components/layouts/public.html.heex
+++ b/lib/chiya_web/components/layouts/public.html.heex
@@ -16,4 +16,4 @@
<.flash_group flash={@flash} />
<%= @inner_content %>
-
\ No newline at end of file
+
diff --git a/lib/chiya_web/components/layouts/root_public.html.heex b/lib/chiya_web/components/layouts/root_public.html.heex
index 1ff835e..fa52f29 100644
--- a/lib/chiya_web/components/layouts/root_public.html.heex
+++ b/lib/chiya_web/components/layouts/root_public.html.heex
@@ -32,10 +32,12 @@
<%= raw(@settings.custom_html) %>
-