Compare commits

..

25 Commits

Author SHA1 Message Date
75df38ff5d add screenshot 2022-11-26 23:20:58 -05:00
b8bb36bfd3 finish faq 2022-11-26 23:14:58 -05:00
8fcbb7aced add steps 2022-11-26 21:32:12 -05:00
1ba5b9ec41 remove step contexts and context notes 2022-11-26 21:32:12 -05:00
443ff86aee use private as default visibility 2022-11-26 21:32:12 -05:00
5ee7071dff rename to memEx 2022-11-26 21:32:12 -05:00
1802e54caf improve hr styling 2022-11-26 20:38:32 -05:00
3dceb17085 display note backlinks in contexts 2022-11-26 20:38:32 -05:00
ca5b29c914 fix home page title 2022-11-26 20:38:31 -05:00
264f13e523 use slugs 2022-11-26 20:38:31 -05:00
e9360fb3d5 fix page titles 2022-11-26 20:37:56 -05:00
44c8cf77bb only show action buttons when appropriate 2022-11-26 20:37:56 -05:00
632c2b3480 add Accounts.is_already_admin? check 2022-11-26 20:37:56 -05:00
852e60dd14 make topbar thinner 2022-11-26 20:37:56 -05:00
85c4559d1f work on steps 2022-11-26 20:37:56 -05:00
7195c7fba6 work on pipelines 2022-11-26 20:37:56 -05:00
ad457b428a improve placeholder text style 2022-11-26 20:37:56 -05:00
cc11491106 work on contexts 2022-11-26 20:37:55 -05:00
a0a0697f2d more changes to notes 2022-11-24 15:52:57 -05:00
2ce4fe3cc8 improve accounts context 2022-11-24 15:52:45 -05:00
f9be5229e7 add search to notes 2022-11-24 15:52:27 -05:00
b95d3039bb remove transition all on input 2022-11-24 15:52:09 -05:00
3c250e9064 alias endpoint 2022-11-24 15:51:58 -05:00
6f80ef35a1 display topbar when user is logged out 2022-11-24 15:51:43 -05:00
1a508a42ef implement notes 2022-11-24 15:51:06 -05:00
84 changed files with 4011 additions and 1414 deletions

View File

@ -1,6 +1,6 @@
kind: pipeline kind: pipeline
type: docker type: docker
name: memex name: memEx
steps: steps:
- name: restore-cache - name: restore-cache

View File

@ -7,7 +7,7 @@
.input-primary { .input-primary {
@apply bg-primary-900; @apply bg-primary-900;
@apply border-primary-900 hover:border-primary-800 active:border-primary-700; @apply border-primary-900 hover:border-primary-800 active:border-primary-700;
@apply text-primary-400; @apply text-primary-400 placeholder-primary-600;
} }
.checkbox { .checkbox {
@ -44,11 +44,7 @@
} }
.hr { .hr {
@apply border border-primary-400 w-full max-w-2xl; @apply mx-auto border border-primary-600 w-full max-w-2xl;
}
.hr-light {
@apply border border-primary-600 w-full max-w-2xl;
} }
.link { .link {

View File

@ -29,7 +29,9 @@ import topbar from '../vendor/topbar'
import MaintainAttrs from './maintain_attrs' import MaintainAttrs from './maintain_attrs'
import Alpine from 'alpinejs' import Alpine from 'alpinejs'
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content') const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
let csrfToken
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
const liveSocket = new LiveSocket('/live', Socket, { const liveSocket = new LiveSocket('/live', Socket, {
dom: { dom: {
onBeforeElUpdated (from, to) { onBeforeElUpdated (from, to) {
@ -45,7 +47,7 @@ window.Alpine = Alpine
Alpine.start() Alpine.start()
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' }) topbar.config({ barThickness: 1, barColors: { 0: '#fff' }, shadowColor: 'rgba(0, 0, 0, .3)' })
window.addEventListener('phx:page-loading-start', info => topbar.show()) window.addEventListener('phx:page-loading-start', info => topbar.show())
window.addEventListener('phx:page-loading-stop', info => topbar.hide()) window.addEventListener('phx:page-loading-stop', info => topbar.hide())

BIN
home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -23,7 +23,7 @@ defmodule Memex.Accounts do
nil nil
""" """
@spec get_user_by_email(String.t()) :: User.t() | nil @spec get_user_by_email(email :: String.t()) :: User.t() | nil
def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email) def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
@doc """ @doc """
@ -38,7 +38,7 @@ defmodule Memex.Accounts do
nil nil
""" """
@spec get_user_by_email_and_password(String.t(), String.t()) :: @spec get_user_by_email_and_password(email :: String.t(), password :: String.t()) ::
User.t() | nil User.t() | nil
def get_user_by_email_and_password(email, password) def get_user_by_email_and_password(email, password)
when is_binary(email) and is_binary(password) do when is_binary(email) and is_binary(password) do
@ -86,7 +86,7 @@ defmodule Memex.Accounts do
[%User{}] [%User{}]
""" """
@spec list_users_by_role(:admin | :user) :: [User.t()] @spec list_users_by_role(User.role()) :: [User.t()]
def list_users_by_role(role) do def list_users_by_role(role) do
role = role |> to_string() role = role |> to_string()
Repo.all(from u in User, where: u.role == ^role, order_by: u.email) Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
@ -106,15 +106,21 @@ defmodule Memex.Accounts do
{:error, %Changeset{}} {:error, %Changeset{}}
""" """
@spec register_user(map()) :: {:ok, User.t()} | {:error, Changeset.t(User.new_user())} @spec register_user(attrs :: map()) :: {:ok, User.t()} | {:error, User.changeset()}
def register_user(attrs) do def register_user(attrs) do
# if no registered users, make first user an admin Multi.new()
role = |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
if Repo.one!(from u in User, select: count(u.id), distinct: true) == 0, |> Multi.insert(:add_user, fn %{users_count: count} ->
do: "admin", # if no registered users, make first user an admin
else: "user" role = if count == 0, do: "admin", else: "user"
%User{} |> User.registration_changeset(attrs |> Map.put("role", role)) |> Repo.insert() User.registration_changeset(attrs) |> User.role_changeset(role)
end)
|> Repo.transaction()
|> case do
{:ok, %{add_user: user}} -> {:ok, user}
{:error, :add_user, changeset, _changes_so_far} -> {:error, changeset}
end
end end
@doc """ @doc """
@ -126,12 +132,10 @@ defmodule Memex.Accounts do
%Changeset{data: %User{}} %Changeset{data: %User{}}
""" """
@spec change_user_registration(User.t() | User.new_user()) :: @spec change_user_registration() :: User.changeset()
Changeset.t(User.t() | User.new_user()) @spec change_user_registration(attrs :: map()) :: User.changeset()
@spec change_user_registration(User.t() | User.new_user(), map()) :: def change_user_registration(attrs \\ %{}),
Changeset.t(User.t() | User.new_user()) do: User.registration_changeset(attrs, hash_password: false)
def change_user_registration(user, attrs \\ %{}),
do: User.registration_changeset(user, attrs, hash_password: false)
## Settings ## Settings
@ -144,7 +148,7 @@ defmodule Memex.Accounts do
%Changeset{data: %User{}} %Changeset{data: %User{}}
""" """
@spec change_user_email(User.t(), map()) :: Changeset.t(User.t()) @spec change_user_email(User.t(), attrs :: map()) :: User.changeset()
def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs) def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
@doc """ @doc """
@ -156,7 +160,7 @@ defmodule Memex.Accounts do
%Changeset{data: %User{}} %Changeset{data: %User{}}
""" """
@spec change_user_role(User.t(), atom()) :: Changeset.t(User.t()) @spec change_user_role(User.t(), User.role()) :: User.changeset()
def change_user_role(user, role), do: User.role_changeset(user, role) def change_user_role(user, role), do: User.role_changeset(user, role)
@doc """ @doc """
@ -172,8 +176,8 @@ defmodule Memex.Accounts do
{:error, %Changeset{}} {:error, %Changeset{}}
""" """
@spec apply_user_email(User.t(), String.t(), map()) :: @spec apply_user_email(User.t(), password :: String.t(), attrs :: map()) ::
{:ok, User.t()} | {:error, Changeset.t(User.t())} {:ok, User.t()} | {:error, User.changeset()}
def apply_user_email(user, password, attrs) do def apply_user_email(user, password, attrs) do
user user
|> User.email_changeset(attrs) |> User.email_changeset(attrs)
@ -187,7 +191,7 @@ defmodule Memex.Accounts do
If the token matches, the user email is updated and the token is deleted. If the token matches, the user email is updated and the token is deleted.
The confirmed_at date is also updated to the current time. The confirmed_at date is also updated to the current time.
""" """
@spec update_user_email(User.t(), String.t()) :: :ok | :error @spec update_user_email(User.t(), token :: String.t()) :: :ok | :error
def update_user_email(user, token) do def update_user_email(user, token) do
context = "change:#{user.email}" context = "change:#{user.email}"
@ -200,7 +204,7 @@ defmodule Memex.Accounts do
end end
end end
@spec user_email_multi(User.t(), String.t(), String.t()) :: Multi.t() @spec user_email_multi(User.t(), email :: String.t(), context :: String.t()) :: Multi.t()
defp user_email_multi(user, email, context) do defp user_email_multi(user, email, context) do
changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset() changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
@ -218,7 +222,8 @@ defmodule Memex.Accounts do
{:ok, %{to: ..., body: ...}} {:ok, %{to: ..., body: ...}}
""" """
@spec deliver_update_email_instructions(User.t(), String.t(), function) :: Job.t() @spec deliver_update_email_instructions(User.t(), current_email :: String.t(), function) ::
Job.t()
def deliver_update_email_instructions(user, current_email, update_email_url_fun) def deliver_update_email_instructions(user, current_email, update_email_url_fun)
when is_function(update_email_url_fun, 1) do when is_function(update_email_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
@ -235,7 +240,7 @@ defmodule Memex.Accounts do
%Changeset{data: %User{}} %Changeset{data: %User{}}
""" """
@spec change_user_password(User.t(), map()) :: Changeset.t(User.t()) @spec change_user_password(User.t(), attrs :: map()) :: User.changeset()
def change_user_password(user, attrs \\ %{}), def change_user_password(user, attrs \\ %{}),
do: User.password_changeset(user, attrs, hash_password: false) do: User.password_changeset(user, attrs, hash_password: false)
@ -251,8 +256,8 @@ defmodule Memex.Accounts do
{:error, %Changeset{}} {:error, %Changeset{}}
""" """
@spec update_user_password(User.t(), String.t(), map()) :: @spec update_user_password(User.t(), password :: String.t(), attrs :: map()) ::
{:ok, User.t()} | {:error, Changeset.t(User.t())} {:ok, User.t()} | {:error, User.changeset()}
def update_user_password(user, password, attrs) do def update_user_password(user, password, attrs) do
changeset = changeset =
user user
@ -278,7 +283,7 @@ defmodule Memex.Accounts do
%Changeset{data: %User{}} %Changeset{data: %User{}}
""" """
@spec change_user_locale(User.t()) :: Changeset.t(User.t()) @spec change_user_locale(User.t()) :: User.changeset()
def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale) def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale)
@doc """ @doc """
@ -294,7 +299,7 @@ defmodule Memex.Accounts do
""" """
@spec update_user_locale(User.t(), locale :: String.t()) :: @spec update_user_locale(User.t(), locale :: String.t()) ::
{:ok, User.t()} | {:error, Changeset.t(User.t())} {:ok, User.t()} | {:error, User.changeset()}
def update_user_locale(user, locale), def update_user_locale(user, locale),
do: user |> User.locale_changeset(locale) |> Repo.update() do: user |> User.locale_changeset(locale) |> Repo.update()
@ -310,7 +315,7 @@ defmodule Memex.Accounts do
%User{} %User{}
""" """
@spec delete_user!(User.t(), User.t()) :: User.t() @spec delete_user!(user_to_delete :: User.t(), User.t()) :: User.t()
def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!() def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!() def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
@ -329,7 +334,7 @@ defmodule Memex.Accounts do
@doc """ @doc """
Gets the user with the given signed token. Gets the user with the given signed token.
""" """
@spec get_user_by_session_token(String.t()) :: User.t() @spec get_user_by_session_token(token :: String.t()) :: User.t()
def get_user_by_session_token(token) do def get_user_by_session_token(token) do
{:ok, query} = UserToken.verify_session_token_query(token) {:ok, query} = UserToken.verify_session_token_query(token)
Repo.one(query) Repo.one(query)
@ -338,7 +343,7 @@ defmodule Memex.Accounts do
@doc """ @doc """
Deletes the signed token with the given context. Deletes the signed token with the given context.
""" """
@spec delete_session_token(String.t()) :: :ok @spec delete_session_token(token :: String.t()) :: :ok
def delete_session_token(token) do def delete_session_token(token) do
Repo.delete_all(UserToken.token_and_context_query(token, "session")) Repo.delete_all(UserToken.token_and_context_query(token, "session"))
:ok :ok
@ -358,10 +363,16 @@ defmodule Memex.Accounts do
""" """
@spec is_admin?(User.t()) :: boolean() @spec is_admin?(User.t()) :: boolean()
def is_admin?(%User{id: user_id}) do def is_admin?(%User{id: user_id}) do
Repo.one(from u in User, where: u.id == ^user_id and u.role == :admin) Repo.exists?(from u in User, where: u.id == ^user_id and u.role == :admin)
|> is_nil()
end end
@doc """
Checks to see if user has the admin role
"""
@spec is_already_admin?(User.t() | nil) :: boolean()
def is_already_admin?(%User{role: :admin}), do: true
def is_already_admin?(_invalid_user), do: false
## Confirmation ## Confirmation
@doc """ @doc """
@ -394,7 +405,7 @@ defmodule Memex.Accounts do
If the token matches, the user account is marked as confirmed If the token matches, the user account is marked as confirmed
and the token is deleted. and the token is deleted.
""" """
@spec confirm_user(String.t()) :: {:ok, User.t()} | atom() @spec confirm_user(token :: String.t()) :: {:ok, User.t()} | atom()
def confirm_user(token) do def confirm_user(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"), with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
%User{} = user <- Repo.one(query), %User{} = user <- Repo.one(query),
@ -443,7 +454,7 @@ defmodule Memex.Accounts do
nil nil
""" """
@spec get_user_by_reset_password_token(String.t()) :: User.t() | nil @spec get_user_by_reset_password_token(token :: String.t()) :: User.t() | nil
def get_user_by_reset_password_token(token) do def get_user_by_reset_password_token(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"), with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
%User{} = user <- Repo.one(query) do %User{} = user <- Repo.one(query) do
@ -465,7 +476,8 @@ defmodule Memex.Accounts do
{:error, %Changeset{}} {:error, %Changeset{}}
""" """
@spec reset_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t(User.t())} @spec reset_user_password(User.t(), attrs :: map()) ::
{:ok, User.t()} | {:error, User.changeset()}
def reset_user_password(user, attrs) do def reset_user_password(user, attrs) do
Multi.new() Multi.new()
|> Multi.update(:user, User.password_changeset(user, attrs)) |> Multi.update(:user, User.password_changeset(user, attrs))

View File

@ -7,7 +7,7 @@ defmodule Memex.Accounts.User do
import Ecto.Changeset import Ecto.Changeset
import MemexWeb.Gettext import MemexWeb.Gettext
alias Ecto.{Changeset, UUID} alias Ecto.{Changeset, UUID}
alias Memex.{Accounts.User, Invites.Invite} alias Memex.Invites.Invite
@derive {Inspect, except: [:password]} @derive {Inspect, except: [:password]}
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@ -25,20 +25,22 @@ defmodule Memex.Accounts.User do
timestamps() timestamps()
end end
@type t :: %User{ @type t :: %__MODULE__{
id: id(), id: id(),
email: String.t(), email: String.t(),
password: String.t(), password: String.t(),
hashed_password: String.t(), hashed_password: String.t(),
confirmed_at: NaiveDateTime.t(), confirmed_at: NaiveDateTime.t(),
role: atom(), role: role(),
invites: [Invite.t()], invites: [Invite.t()],
locale: String.t() | nil, locale: String.t() | nil,
inserted_at: NaiveDateTime.t(), inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t() updated_at: NaiveDateTime.t()
} }
@type new_user :: %User{} @type new_user :: %__MODULE__{}
@type id :: UUID.t() @type id :: UUID.t()
@type changeset :: Changeset.t(t() | new_user())
@type role :: :user | :admin | String.t()
@doc """ @doc """
A user changeset for registration. A user changeset for registration.
@ -57,12 +59,11 @@ defmodule Memex.Accounts.User do
validations on a LiveView form), this option can be set to `false`. validations on a LiveView form), this option can be set to `false`.
Defaults to `true`. Defaults to `true`.
""" """
@spec registration_changeset(t() | new_user(), attrs :: map()) :: Changeset.t(t() | new_user()) @spec registration_changeset(attrs :: map()) :: changeset()
@spec registration_changeset(t() | new_user(), attrs :: map(), opts :: keyword()) :: @spec registration_changeset(attrs :: map(), opts :: keyword()) :: changeset()
Changeset.t(t() | new_user()) def registration_changeset(attrs, opts \\ []) do
def registration_changeset(user, attrs, opts \\ []) do %__MODULE__{}
user |> cast(attrs, [:email, :password, :locale])
|> cast(attrs, [:email, :password, :role, :locale])
|> validate_email() |> validate_email()
|> validate_password(opts) |> validate_password(opts)
end end
@ -71,12 +72,12 @@ defmodule Memex.Accounts.User do
A user changeset for role. A user changeset for role.
""" """
@spec role_changeset(t(), role :: atom()) :: Changeset.t(t()) @spec role_changeset(t() | new_user() | changeset(), role()) :: changeset()
def role_changeset(user, role) do def role_changeset(user, role) do
user |> cast(%{"role" => role}, [:role]) user |> cast(%{"role" => role}, [:role])
end end
@spec validate_email(Changeset.t(t() | new_user())) :: Changeset.t(t() | new_user()) @spec validate_email(changeset()) :: changeset()
defp validate_email(changeset) do defp validate_email(changeset) do
changeset changeset
|> validate_required([:email]) |> validate_required([:email])
@ -88,8 +89,7 @@ defmodule Memex.Accounts.User do
|> unique_constraint(:email) |> unique_constraint(:email)
end end
@spec validate_password(Changeset.t(t() | new_user()), opts :: keyword()) :: @spec validate_password(changeset(), opts :: keyword()) :: changeset()
Changeset.t(t() | new_user())
defp validate_password(changeset, opts) do defp validate_password(changeset, opts) do
changeset changeset
|> validate_required([:password]) |> validate_required([:password])
@ -100,8 +100,7 @@ defmodule Memex.Accounts.User do
|> maybe_hash_password(opts) |> maybe_hash_password(opts)
end end
@spec maybe_hash_password(Changeset.t(t() | new_user()), opts :: keyword()) :: @spec maybe_hash_password(changeset(), opts :: keyword()) :: changeset()
Changeset.t(t() | new_user())
defp maybe_hash_password(changeset, opts) do defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true) hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password) password = get_change(changeset, :password)
@ -120,7 +119,7 @@ defmodule Memex.Accounts.User do
It requires the email to change otherwise an error is added. It requires the email to change otherwise an error is added.
""" """
@spec email_changeset(t(), attrs :: map()) :: Changeset.t(t()) @spec email_changeset(t(), attrs :: map()) :: changeset()
def email_changeset(user, attrs) do def email_changeset(user, attrs) do
user user
|> cast(attrs, [:email]) |> cast(attrs, [:email])
@ -143,8 +142,8 @@ defmodule Memex.Accounts.User do
validations on a LiveView form), this option can be set to `false`. validations on a LiveView form), this option can be set to `false`.
Defaults to `true`. Defaults to `true`.
""" """
@spec password_changeset(t(), attrs :: map()) :: Changeset.t(t()) @spec password_changeset(t(), attrs :: map()) :: changeset()
@spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: Changeset.t(t()) @spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: changeset()
def password_changeset(user, attrs, opts \\ []) do def password_changeset(user, attrs, opts \\ []) do
user user
|> cast(attrs, [:password]) |> cast(attrs, [:password])
@ -155,7 +154,7 @@ defmodule Memex.Accounts.User do
@doc """ @doc """
Confirms the account by setting `confirmed_at`. Confirms the account by setting `confirmed_at`.
""" """
@spec confirm_changeset(t() | Changeset.t(t())) :: Changeset.t(t()) @spec confirm_changeset(t() | changeset()) :: changeset()
def confirm_changeset(user_or_changeset) do def confirm_changeset(user_or_changeset) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
user_or_changeset |> change(confirmed_at: now) user_or_changeset |> change(confirmed_at: now)
@ -168,7 +167,7 @@ defmodule Memex.Accounts.User do
`Bcrypt.no_user_verify/0` to avoid timing attacks. `Bcrypt.no_user_verify/0` to avoid timing attacks.
""" """
@spec valid_password?(t(), String.t()) :: boolean() @spec valid_password?(t(), String.t()) :: boolean()
def valid_password?(%User{hashed_password: hashed_password}, password) def valid_password?(%__MODULE__{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password) Bcrypt.verify_pass(password, hashed_password)
end end
@ -181,7 +180,7 @@ defmodule Memex.Accounts.User do
@doc """ @doc """
Validates the current password otherwise adds an error to the changeset. Validates the current password otherwise adds an error to the changeset.
""" """
@spec validate_current_password(Changeset.t(t()), String.t()) :: Changeset.t(t()) @spec validate_current_password(changeset(), String.t()) :: changeset()
def validate_current_password(changeset, password) do def validate_current_password(changeset, password) do
if valid_password?(changeset.data, password), if valid_password?(changeset.data, password),
do: changeset, do: changeset,
@ -191,7 +190,7 @@ defmodule Memex.Accounts.User do
@doc """ @doc """
A changeset for changing the user's locale A changeset for changing the user's locale
""" """
@spec locale_changeset(t() | Changeset.t(t()), locale :: String.t() | nil) :: Changeset.t(t()) @spec locale_changeset(t() | changeset(), locale :: String.t() | nil) :: changeset()
def locale_changeset(user_or_changeset, locale) do def locale_changeset(user_or_changeset, locale) do
user_or_changeset user_or_changeset
|> cast(%{"locale" => locale}, [:locale]) |> cast(%{"locale" => locale}, [:locale])

View File

@ -4,21 +4,89 @@ defmodule Memex.Contexts do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Memex.Repo alias Ecto.Changeset
alias Memex.{Accounts.User, Contexts.Context, Repo}
alias Memex.Contexts.Context
@doc """ @doc """
Returns the list of contexts. Returns the list of contexts.
## Examples ## Examples
iex> list_contexts() iex> list_contexts(%User{id: 123})
[%Context{}, ...] [%Context{}, ...]
iex> list_contexts("my context", %User{id: 123})
[%Context{slug: "my context"}, ...]
""" """
def list_contexts do @spec list_contexts(User.t()) :: [Context.t()]
Repo.all(Context) @spec list_contexts(search :: String.t() | nil, User.t()) :: [Context.t()]
def list_contexts(search \\ nil, user)
def list_contexts(search, %{id: user_id}) when search |> is_nil() or search == "" do
Repo.all(from c in Context, where: c.user_id == ^user_id, order_by: c.slug)
end
def list_contexts(search, %{id: user_id}) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from c in Context,
where: c.user_id == ^user_id,
where:
fragment(
"search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')",
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)",
^trimmed_search
)
}
)
end
@doc """
Returns the list of public contexts for viewing.
## Examples
iex> list_public_contexts()
[%Context{}, ...]
iex> list_public_contexts("my context")
[%Context{slug: "my context"}, ...]
"""
@spec list_public_contexts() :: [Context.t()]
@spec list_public_contexts(search :: String.t() | nil) :: [Context.t()]
def list_public_contexts(search \\ nil)
def list_public_contexts(search) when search |> is_nil() or search == "" do
Repo.all(from c in Context, where: c.visibility == :public, order_by: c.slug)
end
def list_public_contexts(search) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from c in Context,
where: c.visibility == :public,
where:
fragment(
"search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')",
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)",
^trimmed_search
)
}
)
end end
@doc """ @doc """
@ -28,31 +96,78 @@ defmodule Memex.Contexts do
## Examples ## Examples
iex> get_context!(123) iex> get_context!(123, %User{id: 123})
%Context{} %Context{}
iex> get_context!(456) iex> get_context!(456, %User{id: 123})
** (Ecto.NoResultsError) ** (Ecto.NoResultsError)
""" """
def get_context!(id), do: Repo.get!(Context, id) @spec get_context!(Context.id(), User.t()) :: Context.t()
def get_context!(id, %{id: user_id}) do
Repo.one!(
from c in Context,
where: c.id == ^id,
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
)
end
def get_context!(id, _invalid_user) do
Repo.one!(
from c in Context,
where: c.id == ^id,
where: c.visibility in [:public, :unlisted]
)
end
@doc """
Gets a single context by a slug.
Raises `Ecto.NoResultsError` if the Context does not exist.
## Examples
iex> get_context_by_slug("my-context", %User{id: 123})
%Context{}
iex> get_context_by_slug("my-context", %User{id: 123})
** (Ecto.NoResultsError)
"""
@spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
def get_context_by_slug(slug, %{id: user_id}) do
Repo.one(
from c in Context,
where: c.slug == ^slug,
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
)
end
def get_context_by_slug(slug, _invalid_user) do
Repo.one(
from c in Context,
where: c.slug == ^slug,
where: c.visibility in [:public, :unlisted]
)
end
@doc """ @doc """
Creates a context. Creates a context.
## Examples ## Examples
iex> create_context(%{field: value}) iex> create_context(%{field: value}, %User{id: 123})
{:ok, %Context{}} {:ok, %Context{}}
iex> create_context(%{field: bad_value}) iex> create_context(%{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_context(attrs \\ %{}) do @spec create_context(User.t()) :: {:ok, Context.t()} | {:error, Context.changeset()}
%Context{} @spec create_context(attrs :: map(), User.t()) ::
|> Context.changeset(attrs) {:ok, Context.t()} | {:error, Context.changeset()}
|> Repo.insert() def create_context(attrs \\ %{}, user) do
Context.create_changeset(attrs, user) |> Repo.insert()
end end
@doc """ @doc """
@ -60,16 +175,18 @@ defmodule Memex.Contexts do
## Examples ## Examples
iex> update_context(context, %{field: new_value}) iex> update_context(context, %{field: new_value}, %User{id: 123})
{:ok, %Context{}} {:ok, %Context{}}
iex> update_context(context, %{field: bad_value}) iex> update_context(context, %{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def update_context(%Context{} = context, attrs) do @spec update_context(Context.t(), attrs :: map(), User.t()) ::
{:ok, Context.t()} | {:error, Context.changeset()}
def update_context(%Context{} = context, attrs, user) do
context context
|> Context.changeset(attrs) |> Context.update_changeset(attrs, user)
|> Repo.update() |> Repo.update()
end end
@ -78,15 +195,24 @@ defmodule Memex.Contexts do
## Examples ## Examples
iex> delete_context(context) iex> delete_context(%Context{user_id: 123}, %User{id: 123})
{:ok, %Context{}} {:ok, %Context{}}
iex> delete_context(context) iex> delete_context(%Context{user_id: 123}, %User{role: :admin})
{:ok, %Context{}}
iex> delete_context(%Context{}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def delete_context(%Context{} = context) do @spec delete_context(Context.t(), User.t()) ::
Repo.delete(context) {:ok, Context.t()} | {:error, Context.changeset()}
def delete_context(%Context{user_id: user_id} = context, %{id: user_id}) do
context |> Repo.delete()
end
def delete_context(%Context{} = context, %{role: :admin}) do
context |> Repo.delete()
end end
@doc """ @doc """
@ -98,7 +224,23 @@ defmodule Memex.Contexts do
%Ecto.Changeset{data: %Context{}} %Ecto.Changeset{data: %Context{}}
""" """
def change_context(%Context{} = context, attrs \\ %{}) do @spec change_context(Context.t(), User.t()) :: Context.changeset()
Context.changeset(context, attrs) @spec change_context(Context.t(), attrs :: map(), User.t()) :: Context.changeset()
def change_context(%Context{} = context, attrs \\ %{}, user) do
context |> Context.update_changeset(attrs, user)
end
@doc """
Gets a canonical string representation of the `:tags` field for a Note
"""
@spec get_tags_string(Context.t() | Context.changeset() | [String.t()] | nil) :: String.t()
def get_tags_string(nil), do: ""
def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",")
def get_tags_string(%Context{tags: tags}), do: tags |> get_tags_string()
def get_tags_string(%Changeset{} = changeset) do
changeset
|> Changeset.get_field(:tags)
|> get_tags_string()
end end
end end

View File

@ -1,22 +1,77 @@
defmodule Memex.Contexts.Context do defmodule Memex.Contexts.Context do
@moduledoc """
Represents a document that synthesizes multiple concepts as defined by notes
into a single consideration
"""
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID}
alias Memex.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "contexts" do schema "contexts" do
field :slug, :string
field :content, :string field :content, :string
field :tag, {:array, :string} field :tags, {:array, :string}
field :title, :string field :tags_string, :string, virtual: true
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted] field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User
timestamps() timestamps()
end end
@type t :: %__MODULE__{
slug: slug(),
content: String.t(),
tags: [String.t()] | nil,
tags_string: String.t(),
visibility: :public | :private | :unlisted,
user: User.t() | Ecto.Association.NotLoaded.t(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
@type id :: UUID.t()
@type slug :: String.t()
@type changeset :: Changeset.t(t())
@doc false @doc false
def changeset(context, attrs) do @spec create_changeset(attrs :: map(), User.t()) :: changeset()
context def create_changeset(attrs, %User{id: user_id}) do
|> cast(attrs, [:title, :content, :tag, :visibility]) %__MODULE__{}
|> validate_required([:title, :content, :tag, :visibility]) |> cast(attrs, [:slug, :content, :tags, :visibility])
|> change(user_id: user_id)
|> cast_tags_string(attrs)
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :content, :user_id, :visibility])
end end
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
note
|> cast(attrs, [:slug, :content, :tags, :visibility])
|> cast_tags_string(attrs)
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :content, :visibility])
end
defp cast_tags_string(changeset, %{"tags_string" => tags_string})
when tags_string |> is_binary() do
tags =
tags_string
|> String.split(",", trim: true)
|> Enum.map(fn str -> str |> String.trim() end)
|> Enum.sort()
changeset |> change(tags: tags)
end
defp cast_tags_string(changeset, _attrs), do: changeset
end end

View File

@ -1,20 +0,0 @@
defmodule Memex.Contexts.ContextNote do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "context_notes" do
field :context_id, :binary_id
field :note_id, :binary_id
timestamps()
end
@doc false
def changeset(context_note, attrs) do
context_note
|> cast(attrs, [])
|> validate_required([])
end
end

View File

@ -15,13 +15,16 @@ defmodule Memex.Notes do
iex> list_notes(%User{id: 123}) iex> list_notes(%User{id: 123})
[%Note{}, ...] [%Note{}, ...]
iex> list_notes("my note", %User{id: 123})
[%Note{slug: "my note"}, ...]
""" """
@spec list_notes(User.t() | nil) :: [Note.t()] @spec list_notes(User.t()) :: [Note.t()]
@spec list_notes(search :: String.t() | nil, User.t() | nil) :: [Note.t()] @spec list_notes(search :: String.t() | nil, User.t()) :: [Note.t()]
def list_notes(search \\ nil, user) def list_notes(search \\ nil, user)
def list_notes(search, %{id: user_id}) when search |> is_nil() or search == "" do def list_notes(search, %{id: user_id}) when search |> is_nil() or search == "" do
Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.title) Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.slug)
end end
def list_notes(search, %{id: user_id}) when search |> is_binary() do def list_notes(search, %{id: user_id}) when search |> is_binary() do
@ -52,13 +55,16 @@ defmodule Memex.Notes do
iex> list_public_notes() iex> list_public_notes()
[%Note{}, ...] [%Note{}, ...]
iex> list_public_notes("my note")
[%Note{slug: "my note"}, ...]
""" """
@spec list_public_notes() :: [Note.t()] @spec list_public_notes() :: [Note.t()]
@spec list_public_notes(search :: String.t() | nil) :: [Note.t()] @spec list_public_notes(search :: String.t() | nil) :: [Note.t()]
def list_public_notes(search \\ nil) def list_public_notes(search \\ nil)
def list_public_notes(search) when search |> is_nil() or search == "" do def list_public_notes(search) when search |> is_nil() or search == "" do
Repo.all(from n in Note, where: n.visibility == :public, order_by: n.title) Repo.all(from n in Note, where: n.visibility == :public, order_by: n.slug)
end end
def list_public_notes(search) when search |> is_binary() do def list_public_notes(search) when search |> is_binary() do
@ -113,6 +119,37 @@ defmodule Memex.Notes do
) )
end end
@doc """
Gets a single note by slug.
Raises `Ecto.NoResultsError` if the Note does not exist.
## Examples
iex> get_note_by_slug("my-note", %User{id: 123})
%Note{}
iex> get_note_by_slug("my-note", %User{id: 123})
** (Ecto.NoResultsError)
"""
@spec get_note_by_slug(Note.slug(), User.t()) :: Note.t() | nil
def get_note_by_slug(slug, %{id: user_id}) do
Repo.one(
from n in Note,
where: n.slug == ^slug,
where: n.user_id == ^user_id or n.visibility in [:public, :unlisted]
)
end
def get_note_by_slug(slug, _invalid_user) do
Repo.one(
from n in Note,
where: n.slug == ^slug,
where: n.visibility in [:public, :unlisted]
)
end
@doc """ @doc """
Creates a note. Creates a note.
@ -125,8 +162,8 @@ defmodule Memex.Notes do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
@spec create_note(User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} @spec create_note(User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
@spec create_note(attrs :: map(), User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} @spec create_note(attrs :: map(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
def create_note(attrs \\ %{}, user) do def create_note(attrs \\ %{}, user) do
Note.create_changeset(attrs, user) |> Repo.insert() Note.create_changeset(attrs, user) |> Repo.insert()
end end
@ -144,7 +181,7 @@ defmodule Memex.Notes do
""" """
@spec update_note(Note.t(), attrs :: map(), User.t()) :: @spec update_note(Note.t(), attrs :: map(), User.t()) ::
{:ok, Note.t()} | {:error, Changeset.t()} {:ok, Note.t()} | {:error, Note.changeset()}
def update_note(%Note{} = note, attrs, user) do def update_note(%Note{} = note, attrs, user) do
note note
|> Note.update_changeset(attrs, user) |> Note.update_changeset(attrs, user)
@ -156,18 +193,25 @@ defmodule Memex.Notes do
## Examples ## Examples
iex> delete_note(note, %User{id: 123}) iex> delete_note(%Note{user_id: 123}, %User{id: 123})
{:ok, %Note{}} {:ok, %Note{}}
iex> delete_note(note, %User{id: 123}) iex> delete_note(%Note{}, %User{role: :admin})
{:ok, %Note{}}
iex> delete_note(%Note{}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()} @spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do
note |> Repo.delete() note |> Repo.delete()
end end
def delete_note(%Note{} = note, %{role: :admin}) do
note |> Repo.delete()
end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking note changes. Returns an `%Ecto.Changeset{}` for tracking note changes.
@ -176,12 +220,12 @@ defmodule Memex.Notes do
iex> change_note(note, %User{id: 123}) iex> change_note(note, %User{id: 123})
%Ecto.Changeset{data: %Note{}} %Ecto.Changeset{data: %Note{}}
iex> change_note(note, %{title: "new title"}, %User{id: 123}) iex> change_note(note, %{slug: "new slug"}, %User{id: 123})
%Ecto.Changeset{data: %Note{}} %Ecto.Changeset{data: %Note{}}
""" """
@spec change_note(Note.t(), User.t()) :: Changeset.t(Note.t()) @spec change_note(Note.t(), User.t()) :: Note.changeset()
@spec change_note(Note.t(), attrs :: map(), User.t()) :: Changeset.t(Note.t()) @spec change_note(Note.t(), attrs :: map(), User.t()) :: Note.changeset()
def change_note(%Note{} = note, attrs \\ %{}, user) do def change_note(%Note{} = note, attrs \\ %{}, user) do
note |> Note.update_changeset(attrs, user) note |> Note.update_changeset(attrs, user)
end end
@ -189,7 +233,7 @@ defmodule Memex.Notes do
@doc """ @doc """
Gets a canonical string representation of the `:tags` field for a Note Gets a canonical string representation of the `:tags` field for a Note
""" """
@spec get_tags_string(Note.t() | Changeset.t() | [String.t()] | nil) :: String.t() @spec get_tags_string(Note.t() | Note.changeset() | [String.t()] | nil) :: String.t()
def get_tags_string(nil), do: "" def get_tags_string(nil), do: ""
def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",") def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",")
def get_tags_string(%Note{tags: tags}), do: tags |> get_tags_string() def get_tags_string(%Note{tags: tags}), do: tags |> get_tags_string()

View File

@ -4,16 +4,17 @@ defmodule Memex.Notes.Note do
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID} alias Ecto.{Changeset, UUID}
alias Memex.{Accounts.User, Notes.Note} alias Memex.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "notes" do schema "notes" do
field :slug, :string
field :content, :string field :content, :string
field :tags, {:array, :string} field :tags, {:array, :string}
field :tags_string, :string, virtual: true field :tags_string, :string, virtual: true
field :title, :string
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted] field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User belongs_to :user, User
@ -21,29 +22,47 @@ defmodule Memex.Notes.Note do
timestamps() timestamps()
end end
@type t :: %Note{} @type t :: %__MODULE__{
slug: slug(),
content: String.t(),
tags: [String.t()] | nil,
tags_string: String.t(),
visibility: :public | :private | :unlisted,
user: User.t() | Ecto.Association.NotLoaded.t(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
@type id :: UUID.t() @type id :: UUID.t()
@type slug :: String.t()
@type changeset :: Changeset.t(t()) @type changeset :: Changeset.t(t())
@doc false @doc false
@spec create_changeset(attrs :: map(), User.t()) :: changeset() @spec create_changeset(attrs :: map(), User.t()) :: changeset()
def create_changeset(attrs, %User{id: user_id}) do def create_changeset(attrs, %User{id: user_id}) do
%Note{} %__MODULE__{}
|> cast(attrs, [:title, :content, :tags, :visibility]) |> cast(attrs, [:slug, :content, :tags, :visibility])
|> change(user_id: user_id) |> change(user_id: user_id)
|> cast_tags_string(attrs) |> cast_tags_string(attrs)
|> validate_required([:title, :content, :user_id, :visibility]) |> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :content, :user_id, :visibility])
end end
@spec update_changeset(Note.t(), attrs :: map(), User.t()) :: changeset() @spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
note note
|> cast(attrs, [:title, :content, :tags, :visibility]) |> cast(attrs, [:slug, :content, :tags, :visibility])
|> cast_tags_string(attrs) |> cast_tags_string(attrs)
|> validate_required([:title, :content, :visibility]) |> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :content, :visibility])
end end
defp cast_tags_string(changeset, %{"tags_string" => tags_string}) when is_binary(tags_string) do defp cast_tags_string(changeset, %{"tags_string" => tags_string})
when tags_string |> is_binary() do
tags = tags =
tags_string tags_string
|> String.split(",", trim: true) |> String.split(",", trim: true)

View File

@ -4,21 +4,88 @@ defmodule Memex.Pipelines do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Memex.Repo alias Ecto.Changeset
alias Memex.{Accounts.User, Pipelines.Pipeline, Repo}
alias Memex.Pipelines.Pipeline
@doc """ @doc """
Returns the list of pipelines. Returns the list of pipelines.
## Examples ## Examples
iex> list_pipelines() iex> list_pipelines(%User{id: 123})
[%Pipeline{}, ...] [%Pipeline{}, ...]
iex> list_pipelines("my pipeline", %User{id: 123})
[%Pipeline{slug: "my pipeline"}, ...]
""" """
def list_pipelines do @spec list_pipelines(User.t()) :: [Pipeline.t()]
Repo.all(Pipeline) @spec list_pipelines(search :: String.t() | nil, User.t()) :: [Pipeline.t()]
def list_pipelines(search \\ nil, user)
def list_pipelines(search, %{id: user_id}) when search |> is_nil() or search == "" do
Repo.all(from p in Pipeline, where: p.user_id == ^user_id, order_by: p.slug)
end
def list_pipelines(search, %{id: user_id}) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from p in Pipeline,
where: p.user_id == ^user_id,
where:
fragment(
"search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')",
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)",
^trimmed_search
)
}
)
end
@doc """
Returns the list of public pipelines for viewing
## Examples
iex> list_public_pipelines()
[%Pipeline{}, ...]
iex> list_public_pipelines("my pipeline")
[%Pipeline{slug: "my pipeline"}, ...]
"""
@spec list_public_pipelines() :: [Pipeline.t()]
@spec list_public_pipelines(search :: String.t() | nil) :: [Pipeline.t()]
def list_public_pipelines(search \\ nil)
def list_public_pipelines(search) when search |> is_nil() or search == "" do
Repo.all(from p in Pipeline, where: p.visibility == :public, order_by: p.slug)
end
def list_public_pipelines(search) when search |> is_binary() do
trimmed_search = String.trim(search)
Repo.all(
from p in Pipeline,
where: p.visibility == :public,
where:
fragment(
"search @@ to_tsquery(websearch_to_tsquery(?)::text || ':*')",
^trimmed_search
),
order_by: {
:desc,
fragment(
"ts_rank_cd(search, to_tsquery(websearch_to_tsquery(?)::text || ':*'), 4)",
^trimmed_search
)
}
)
end end
@doc """ @doc """
@ -28,31 +95,78 @@ defmodule Memex.Pipelines do
## Examples ## Examples
iex> get_pipeline!(123) iex> get_pipeline!(123, %User{id: 123})
%Pipeline{} %Pipeline{}
iex> get_pipeline!(456) iex> get_pipeline!(456, %User{id: 123})
** (Ecto.NoResultsError) ** (Ecto.NoResultsError)
""" """
def get_pipeline!(id), do: Repo.get!(Pipeline, id) @spec get_pipeline!(Pipeline.id(), User.t()) :: Pipeline.t()
def get_pipeline!(id, %{id: user_id}) do
Repo.one!(
from p in Pipeline,
where: p.id == ^id,
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
)
end
def get_pipeline!(id, _invalid_user) do
Repo.one!(
from p in Pipeline,
where: p.id == ^id,
where: p.visibility in [:public, :unlisted]
)
end
@doc """
Gets a single pipeline by it's slug.
Raises `Ecto.NoResultsError` if the Pipeline does not exist.
## Examples
iex> get_pipeline_by_slug("my-pipeline", %User{id: 123})
%Pipeline{}
iex> get_pipeline_by_slug("my-pipeline", %User{id: 123})
** (Ecto.NoResultsError)
"""
@spec get_pipeline_by_slug(Pipeline.slug(), User.t()) :: Pipeline.t() | nil
def get_pipeline_by_slug(slug, %{id: user_id}) do
Repo.one(
from p in Pipeline,
where: p.slug == ^slug,
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
)
end
def get_pipeline_by_slug(slug, _invalid_user) do
Repo.one(
from p in Pipeline,
where: p.slug == ^slug,
where: p.visibility in [:public, :unlisted]
)
end
@doc """ @doc """
Creates a pipeline. Creates a pipeline.
## Examples ## Examples
iex> create_pipeline(%{field: value}) iex> create_pipeline(%{field: value}, %User{id: 123})
{:ok, %Pipeline{}} {:ok, %Pipeline{}}
iex> create_pipeline(%{field: bad_value}) iex> create_pipeline(%{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_pipeline(attrs \\ %{}) do @spec create_pipeline(User.t()) :: {:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
%Pipeline{} @spec create_pipeline(attrs :: map(), User.t()) ::
|> Pipeline.changeset(attrs) {:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
|> Repo.insert() def create_pipeline(attrs \\ %{}, user) do
Pipeline.create_changeset(attrs, user) |> Repo.insert()
end end
@doc """ @doc """
@ -60,16 +174,18 @@ defmodule Memex.Pipelines do
## Examples ## Examples
iex> update_pipeline(pipeline, %{field: new_value}) iex> update_pipeline(pipeline, %{field: new_value}, %User{id: 123})
{:ok, %Pipeline{}} {:ok, %Pipeline{}}
iex> update_pipeline(pipeline, %{field: bad_value}) iex> update_pipeline(pipeline, %{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def update_pipeline(%Pipeline{} = pipeline, attrs) do @spec update_pipeline(Pipeline.t(), attrs :: map(), User.t()) ::
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
def update_pipeline(%Pipeline{} = pipeline, attrs, user) do
pipeline pipeline
|> Pipeline.changeset(attrs) |> Pipeline.update_changeset(attrs, user)
|> Repo.update() |> Repo.update()
end end
@ -78,15 +194,24 @@ defmodule Memex.Pipelines do
## Examples ## Examples
iex> delete_pipeline(pipeline) iex> delete_pipeline(%Pipeline{user_id: 123}, %User{id: 123})
{:ok, %Pipeline{}} {:ok, %Pipeline{}}
iex> delete_pipeline(pipeline) iex> delete_pipeline(%Pipeline{}, %User{role: :admin})
{:ok, %Pipeline{}}
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def delete_pipeline(%Pipeline{} = pipeline) do @spec delete_pipeline(Pipeline.t(), User.t()) ::
Repo.delete(pipeline) {:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
def delete_pipeline(%Pipeline{user_id: user_id} = pipeline, %{id: user_id}) do
pipeline |> Repo.delete()
end
def delete_pipeline(%Pipeline{} = pipeline, %{role: :admin}) do
pipeline |> Repo.delete()
end end
@doc """ @doc """
@ -94,11 +219,30 @@ defmodule Memex.Pipelines do
## Examples ## Examples
iex> change_pipeline(pipeline) iex> change_pipeline(pipeline, %User{id: 123})
%Ecto.Changeset{data: %Pipeline{}}
iex> change_pipeline(pipeline, %{slug: "new slug"}, %User{id: 123})
%Ecto.Changeset{data: %Pipeline{}} %Ecto.Changeset{data: %Pipeline{}}
""" """
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}) do @spec change_pipeline(Pipeline.t(), User.t()) :: Pipeline.changeset()
Pipeline.changeset(pipeline, attrs) @spec change_pipeline(Pipeline.t(), attrs :: map(), User.t()) :: Pipeline.changeset()
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
pipeline |> Pipeline.update_changeset(attrs, user)
end
@doc """
Gets a canonical string representation of the `:tags` field for a Pipeline
"""
@spec get_tags_string(Pipeline.t() | Pipeline.changeset() | [String.t()] | nil) :: String.t()
def get_tags_string(nil), do: ""
def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",")
def get_tags_string(%Pipeline{tags: tags}), do: tags |> get_tags_string()
def get_tags_string(%Changeset{} = changeset) do
changeset
|> Changeset.get_field(:tags)
|> get_tags_string()
end end
end end

View File

@ -1,21 +1,78 @@
defmodule Memex.Pipelines.Pipeline do defmodule Memex.Pipelines.Pipeline do
@moduledoc """
Represents a chain of considerations to take to accomplish a task
"""
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID}
alias Memex.{Accounts.User, Pipelines.Steps.Step}
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "pipelines" do schema "pipelines" do
field :slug, :string
field :description, :string field :description, :string
field :title, :string field :tags, {:array, :string}
field :tags_string, :string, virtual: true
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted] field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User
has_many :steps, Step, preload_order: [asc: :position]
timestamps() timestamps()
end end
@type t :: %__MODULE__{
slug: slug(),
description: String.t(),
tags: [String.t()] | nil,
tags_string: String.t(),
visibility: :public | :private | :unlisted,
user: User.t() | Ecto.Association.NotLoaded.t(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
@type id :: UUID.t()
@type slug :: String.t()
@type changeset :: Changeset.t(t())
@doc false @doc false
def changeset(pipeline, attrs) do @spec create_changeset(attrs :: map(), User.t()) :: changeset()
pipeline def create_changeset(attrs, %User{id: user_id}) do
|> cast(attrs, [:title, :description, :visibility]) %__MODULE__{}
|> validate_required([:title, :description, :visibility]) |> cast(attrs, [:slug, :description, :tags, :visibility])
|> change(user_id: user_id)
|> cast_tags_string(attrs)
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :user_id, :visibility])
end end
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
def update_changeset(%{user_id: user_id} = pipeline, attrs, %User{id: user_id}) do
pipeline
|> cast(attrs, [:slug, :description, :tags, :visibility])
|> cast_tags_string(attrs)
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
)
|> validate_required([:slug, :visibility])
end
defp cast_tags_string(changeset, %{"tags_string" => tags_string})
when tags_string |> is_binary() do
tags =
tags_string
|> String.split(",", trim: true)
|> Enum.map(fn str -> str |> String.trim() end)
|> Enum.sort()
changeset |> change(tags: tags)
end
defp cast_tags_string(changeset, _attrs), do: changeset
end end

View File

@ -0,0 +1,71 @@
defmodule Memex.Pipelines.Steps.Step do
@moduledoc """
Represents a step taken while executing a pipeline
"""
use Ecto.Schema
import Ecto.Changeset
alias Ecto.{Changeset, UUID}
alias Memex.{Accounts.User, Pipelines.Pipeline}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "steps" do
field :title, :string
field :content, :string
field :position, :integer
belongs_to :pipeline, Pipeline
belongs_to :user, User
timestamps()
end
@type t :: %__MODULE__{
title: String.t(),
content: String.t(),
position: non_neg_integer(),
pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
pipeline_id: Pipeline.id(),
user: User.t() | Ecto.Association.NotLoaded.t(),
user_id: User.id(),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
@type id :: UUID.t()
@type changeset :: Changeset.t(t())
@doc false
@spec create_changeset(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
changeset()
def create_changeset(attrs, position, %Pipeline{id: pipeline_id, user_id: user_id}, %User{
id: user_id
}) do
%__MODULE__{}
|> cast(attrs, [:title, :content])
|> change(pipeline_id: pipeline_id, user_id: user_id, position: position)
|> validate_required([:title, :content, :user_id, :position])
end
@spec update_changeset(t(), attrs :: map(), User.t()) ::
changeset()
def update_changeset(
%{user_id: user_id} = step,
attrs,
%User{id: user_id}
) do
step
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content, :user_id, :position])
end
@spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset()
def position_changeset(
%{user_id: user_id} = step,
position,
%User{id: user_id}
) do
step
|> change(position: position)
|> validate_required([:title, :content, :user_id, :position])
end
end

View File

@ -0,0 +1,238 @@
defmodule Memex.Pipelines.Steps do
@moduledoc """
The context for steps within a pipeline
"""
import Ecto.Query, warn: false
alias Ecto.Multi
alias Memex.{Accounts.User, Repo}
alias Memex.Pipelines.{Pipeline, Steps.Step}
@doc """
Returns the list of steps.
## Examples
iex> list_steps(%User{id: 123})
[%Step{}, ...]
iex> list_steps("my step", %User{id: 123})
[%Step{title: "my step"}, ...]
"""
@spec list_steps(Pipeline.t(), User.t()) :: [Step.t()]
def list_steps(%{id: pipeline_id}, %{id: user_id}) do
Repo.all(
from s in Step,
where: s.pipeline_id == ^pipeline_id,
where: s.user_id == ^user_id,
order_by: s.position
)
end
def list_steps(%{id: pipeline_id, visibility: visibility}, _invalid_user)
when visibility in [:unlisted, :public] do
Repo.all(
from s in Step,
where: s.pipeline_id == ^pipeline_id,
order_by: s.position
)
end
@doc """
Preloads the `:steps` field on a Memex.Pipelines.Pipeline
"""
@spec preload_steps(Pipeline.t(), User.t()) :: Pipeline.t()
def preload_steps(pipeline, user) do
%{pipeline | steps: list_steps(pipeline, user)}
end
@doc """
Gets a single step.
Raises `Ecto.NoResultsError` if the Step does not exist.
## Examples
iex> get_step!(123, %User{id: 123})
%Step{}
iex> get_step!(456, %User{id: 123})
** (Ecto.NoResultsError)
"""
@spec get_step!(Step.id(), User.t()) :: Step.t()
def get_step!(id, %{id: user_id}) do
Repo.one!(from n in Step, where: n.id == ^id, where: n.user_id == ^user_id)
end
def get_step!(id, _invalid_user) do
Repo.one!(
from n in Step,
where: n.id == ^id,
where: n.visibility in [:public, :unlisted]
)
end
@doc """
Creates a step.
## Examples
iex> create_step(%{field: value}, %User{id: 123})
{:ok, %Step{}}
iex> create_step(%{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@spec create_step(position :: non_neg_integer(), Pipeline.t(), User.t()) ::
{:ok, Step.t()} | {:error, Step.changeset()}
@spec create_step(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
{:ok, Step.t()} | {:error, Step.changeset()}
def create_step(attrs \\ %{}, position, pipeline, user) do
Step.create_changeset(attrs, position, pipeline, user) |> Repo.insert()
end
@doc """
Updates a step.
## Examples
iex> update_step(step, %{field: new_value}, %User{id: 123})
{:ok, %Step{}}
iex> update_step(step, %{field: bad_value}, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@spec update_step(Step.t(), attrs :: map(), User.t()) ::
{:ok, Step.t()} | {:error, Step.changeset()}
def update_step(%Step{} = step, attrs, user) do
step
|> Step.update_changeset(attrs, user)
|> Repo.update()
end
@doc """
Deletes a step.
## Examples
iex> delete_step(%Step{user_id: 123}, %User{id: 123})
{:ok, %Step{}}
iex> delete_step(%Step{}, %User{role: :admin})
{:ok, %Step{}}
iex> delete_step(%Step{}, %User{id: 123})
{:error, %Ecto.Changeset{}}
"""
@spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()}
def delete_step(%Step{user_id: user_id} = step, %{id: user_id}) do
delete_step(step)
end
def delete_step(%Step{} = step, %{role: :admin}) do
delete_step(step)
end
defp delete_step(step) do
Multi.new()
|> Multi.delete(:delete_step, step)
|> Multi.update_all(
:reorder_steps,
fn %{delete_step: %{position: position, pipeline_id: pipeline_id}} ->
from s in Step,
where: s.pipeline_id == ^pipeline_id,
where: s.position > ^position,
update: [set: [position: s.position - 1]]
end,
[]
)
|> Repo.transaction()
|> case do
{:ok, %{delete_step: step}} -> {:ok, step}
{:error, :delete_step, changeset, _changes_so_far} -> {:error, changeset}
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking step changes.
## Examples
iex> change_step(step, %User{id: 123})
%Ecto.Changeset{data: %Step{}}
iex> change_step(step, %{title: "new title"}, %User{id: 123})
%Ecto.Changeset{data: %Step{}}
"""
@spec change_step(Step.t(), User.t()) :: Step.changeset()
@spec change_step(Step.t(), attrs :: map(), User.t()) :: Step.changeset()
def change_step(%Step{} = step, attrs \\ %{}, user) do
step |> Step.update_changeset(attrs, user)
end
@spec reorder_step(Step.t(), :up | :down, User.t()) ::
{:ok, Step.t()} | {:error, Step.changeset()}
def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step}
def reorder_step(
%Step{position: position, pipeline_id: pipeline_id, user_id: user_id} = step,
:up,
%{id: user_id} = user
) do
Multi.new()
|> Multi.update_all(
:reorder_steps,
from(s in Step,
where: s.pipeline_id == ^pipeline_id,
where: s.position == ^position - 1,
update: [set: [position: ^position]]
),
[]
)
|> Multi.update(
:update_step,
step |> Step.position_changeset(position - 1, user)
)
|> Repo.transaction()
|> case do
{:ok, %{update_step: step}} -> {:ok, step}
{:error, :update_step, changeset, _changes_so_far} -> {:error, changeset}
end
end
def reorder_step(
%Step{pipeline_id: pipeline_id, position: position, user_id: user_id} = step,
:down,
%{id: user_id} = user
) do
Multi.new()
|> Multi.one(
:step_count,
from(s in Step, where: s.pipeline_id == ^pipeline_id, distinct: true, select: count(s.id))
)
|> Multi.update_all(
:reorder_steps,
from(s in Step,
where: s.pipeline_id == ^pipeline_id,
where: s.position == ^position + 1,
update: [set: [position: ^position]]
),
[]
)
|> Multi.update(:update_step, fn %{step_count: step_count} ->
new_position = if position >= step_count - 1, do: position, else: position + 1
step |> Step.position_changeset(new_position, user)
end)
|> Repo.transaction()
|> case do
{:ok, %{update_step: step}} -> {:ok, step}
{:error, :update_step, changeset, _changes_so_far} -> {:error, changeset}
end
end
end

View File

@ -1,104 +0,0 @@
defmodule Memex.Steps do
@moduledoc """
The Steps context.
"""
import Ecto.Query, warn: false
alias Memex.Repo
alias Memex.Steps.Step
@doc """
Returns the list of steps.
## Examples
iex> list_steps()
[%Step{}, ...]
"""
def list_steps do
Repo.all(Step)
end
@doc """
Gets a single step.
Raises `Ecto.NoResultsError` if the Step does not exist.
## Examples
iex> get_step!(123)
%Step{}
iex> get_step!(456)
** (Ecto.NoResultsError)
"""
def get_step!(id), do: Repo.get!(Step, id)
@doc """
Creates a step.
## Examples
iex> create_step(%{field: value})
{:ok, %Step{}}
iex> create_step(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_step(attrs \\ %{}) do
%Step{}
|> Step.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a step.
## Examples
iex> update_step(step, %{field: new_value})
{:ok, %Step{}}
iex> update_step(step, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_step(%Step{} = step, attrs) do
step
|> Step.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a step.
## Examples
iex> delete_step(step)
{:ok, %Step{}}
iex> delete_step(step)
{:error, %Ecto.Changeset{}}
"""
def delete_step(%Step{} = step) do
Repo.delete(step)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking step changes.
## Examples
iex> change_step(step)
%Ecto.Changeset{data: %Step{}}
"""
def change_step(%Step{} = step, attrs \\ %{}) do
Step.changeset(step, attrs)
end
end

View File

@ -1,22 +0,0 @@
defmodule Memex.Steps.Step do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "steps" do
field :description, :string
field :position, :integer
field :title, :string
field :pipeline_id, :binary_id
timestamps()
end
@doc false
def changeset(step, attrs) do
step
|> cast(attrs, [:title, :description, :position])
|> validate_required([:title, :description, :position])
end
end

View File

@ -1,20 +0,0 @@
defmodule Memex.Steps.StepContext do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "step_contexts" do
field :step_id, :binary_id
field :context_id, :binary_id
timestamps()
end
@doc false
def changeset(step_context, attrs) do
step_context
|> cast(attrs, [])
|> validate_required([])
end
end

View File

@ -0,0 +1,44 @@
defmodule MemexWeb.Components.ContextContent do
@moduledoc """
Display the content for a context
"""
use MemexWeb, :component
alias Memex.Contexts.Context
alias Phoenix.HTML
attr :context, Context, required: true
def context_content(assigns) do
~H"""
<div
id={"show-context-content-#{@context.id}"}
class="input input-primary h-128 min-h-128 inline-block"
phx-hook="MaintainAttrs"
phx-update="ignore"
readonly
phx-no-format
><p class="inline"><%= add_links_to_content(@context.content) %></p></div>
"""
end
defp add_links_to_content(content) do
Regex.replace(
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
content,
fn _whole_match, slug ->
link =
HTML.Link.link(
"[[#{slug}]]",
to: Routes.note_show_path(Endpoint, :show, slug),
class: "link inline",
data: [qa: "context-note-#{slug}"]
)
|> HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
"</p>#{link}<p class=\"inline\">"
end
)
|> HTML.raw()
end
end

View File

@ -0,0 +1,135 @@
defmodule MemexWeb.Components.ContextsTableComponent do
@moduledoc """
A component that displays a list of contexts
"""
use MemexWeb, :live_component
alias Ecto.UUID
alias Memex.{Accounts.User, Contexts, Contexts.Context}
alias Phoenix.LiveView.{Rendered, Socket}
@impl true
@spec update(
%{
required(:id) => UUID.t(),
required(:current_user) => User.t(),
required(:contexts) => [Context.t()],
optional(any()) => any()
},
Socket.t()
) :: {:ok, Socket.t()}
def update(%{id: _id, contexts: _contexts, current_user: _current_user} = assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign_new(:actions, fn -> [] end)
|> display_contexts()
{:ok, socket}
end
defp display_contexts(
%{
assigns: %{
contexts: contexts,
current_user: current_user,
actions: actions
}
} = socket
) do
columns =
if actions == [] or current_user |> is_nil() do
[]
else
[%{label: nil, key: :actions, sortable: false}]
end
columns = [
%{label: gettext("slug"), key: :slug},
%{label: gettext("content"), key: :content},
%{label: gettext("tags"), key: :tags},
%{label: gettext("visibility"), key: :visibility}
| columns
]
rows =
contexts
|> Enum.map(fn context ->
context
|> get_row_data_for_context(%{
columns: columns,
current_user: current_user,
actions: actions
})
end)
socket |> assign(columns: columns, rows: rows)
end
@impl true
def render(assigns) do
~H"""
<div class="w-full">
<.live_component
module={MemexWeb.Components.TableComponent}
id={@id}
columns={@columns}
rows={@rows}
/>
</div>
"""
end
@spec get_row_data_for_context(Context.t(), additional_data :: map()) :: map()
defp get_row_data_for_context(context, %{columns: columns} = additional_data) do
columns
|> Map.new(fn %{key: key} ->
{key, get_value_for_key(key, context, additional_data)}
end)
end
@spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
any() | {any(), Rendered.t()}
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
assigns = %{slug: slug}
slug_block = ~H"""
<.link
navigate={Routes.context_show_path(Endpoint, :show, @slug)}
class="link"
data-qa={"context-show-#{@slug}"}
>
<%= @slug %>
</.link>
"""
{slug, slug_block}
end
defp get_value_for_key(:content, %{content: content}, _additional_data) do
assigns = %{content: content}
content_block = ~H"""
<div class="truncate max-w-sm">
<%= @content %>
</div>
"""
{content, content_block}
end
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
tags |> Contexts.get_tags_string()
end
defp get_value_for_key(:actions, context, %{actions: actions}) do
assigns = %{actions: actions, context: context}
~H"""
<div class="flex justify-center items-center space-x-4">
<%= render_slot(@actions, @context) %>
</div>
"""
end
defp get_value_for_key(key, context, _additional_data), do: context |> Map.get(key)
end

View File

@ -1,29 +0,0 @@
defmodule MemexWeb.Components.NoteCard do
@moduledoc """
Display card for an note
"""
use MemexWeb, :component
def note_card(assigns) do
~H"""
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out">
<h1 class="title text-xl">
<%= @note.name %>
</h1>
<h2 class="title text-md">
<%= gettext("visibility: %{visibility}", visibility: @note.visibility) %>
</h2>
<%= if @inner_block do %>
<div class="flex space-x-4 justify-center items-center">
<%= render_slot(@inner_block) %>
</div>
<% end %>
</div>
"""
end
end

View File

@ -44,7 +44,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
end end
columns = [ columns = [
%{label: gettext("title"), key: :title}, %{label: gettext("slug"), key: :slug},
%{label: gettext("content"), key: :content}, %{label: gettext("content"), key: :content},
%{label: gettext("tags"), key: :tags}, %{label: gettext("tags"), key: :tags},
%{label: gettext("visibility"), key: :visibility} %{label: gettext("visibility"), key: :visibility}
@ -89,20 +89,20 @@ defmodule MemexWeb.Components.NotesTableComponent do
@spec get_value_for_key(atom(), Note.t(), additional_data :: map()) :: @spec get_value_for_key(atom(), Note.t(), additional_data :: map()) ::
any() | {any(), Rendered.t()} any() | {any(), Rendered.t()}
defp get_value_for_key(:title, %{id: id, title: title}, _additional_data) do defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
assigns = %{id: id, title: title} assigns = %{slug: slug}
title_block = ~H""" slug_block = ~H"""
<.link <.link
navigate={Routes.note_show_path(Endpoint, :show, @id)} navigate={Routes.note_show_path(Endpoint, :show, @slug)}
class="link" class="link"
data-qa={"note-show-#{@id}"} data-qa={"note-show-#{@slug}"}
> >
<%= @title %> <%= @slug %>
</.link> </.link>
""" """
{title, title_block} {slug, slug_block}
end end
defp get_value_for_key(:content, %{content: content}, _additional_data) do defp get_value_for_key(:content, %{content: content}, _additional_data) do

View File

@ -0,0 +1,135 @@
defmodule MemexWeb.Components.PipelinesTableComponent do
@moduledoc """
A component that displays a list of pipelines
"""
use MemexWeb, :live_component
alias Ecto.UUID
alias Memex.{Accounts.User, Pipelines, Pipelines.Pipeline}
alias Phoenix.LiveView.{Rendered, Socket}
@impl true
@spec update(
%{
required(:id) => UUID.t(),
required(:current_user) => User.t(),
required(:pipelines) => [Pipeline.t()],
optional(any()) => any()
},
Socket.t()
) :: {:ok, Socket.t()}
def update(%{id: _id, pipelines: _pipelines, current_user: _current_user} = assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign_new(:actions, fn -> [] end)
|> display_pipelines()
{:ok, socket}
end
defp display_pipelines(
%{
assigns: %{
pipelines: pipelines,
current_user: current_user,
actions: actions
}
} = socket
) do
columns =
if actions == [] or current_user |> is_nil() do
[]
else
[%{label: nil, key: :actions, sortable: false}]
end
columns = [
%{label: gettext("slug"), key: :slug},
%{label: gettext("description"), key: :description},
%{label: gettext("tags"), key: :tags},
%{label: gettext("visibility"), key: :visibility}
| columns
]
rows =
pipelines
|> Enum.map(fn pipeline ->
pipeline
|> get_row_data_for_pipeline(%{
columns: columns,
current_user: current_user,
actions: actions
})
end)
socket |> assign(columns: columns, rows: rows)
end
@impl true
def render(assigns) do
~H"""
<div class="w-full">
<.live_component
module={MemexWeb.Components.TableComponent}
id={@id}
columns={@columns}
rows={@rows}
/>
</div>
"""
end
@spec get_row_data_for_pipeline(Pipeline.t(), additional_data :: map()) :: map()
defp get_row_data_for_pipeline(pipeline, %{columns: columns} = additional_data) do
columns
|> Map.new(fn %{key: key} ->
{key, get_value_for_key(key, pipeline, additional_data)}
end)
end
@spec get_value_for_key(atom(), Pipeline.t(), additional_data :: map()) ::
any() | {any(), Rendered.t()}
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
assigns = %{slug: slug}
slug_block = ~H"""
<.link
navigate={Routes.pipeline_show_path(Endpoint, :show, @slug)}
class="link"
data-qa={"pipeline-show-#{@slug}"}
>
<%= @slug %>
</.link>
"""
{slug, slug_block}
end
defp get_value_for_key(:description, %{description: description}, _additional_data) do
assigns = %{description: description}
description_block = ~H"""
<div class="truncate max-w-sm">
<%= @description %>
</div>
"""
{description, description_block}
end
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
tags |> Pipelines.get_tags_string()
end
defp get_value_for_key(:actions, pipeline, %{actions: actions}) do
assigns = %{actions: actions, pipeline: pipeline}
~H"""
<div class="flex justify-center items-center space-x-4">
<%= render_slot(@actions, @pipeline) %>
</div>
"""
end
defp get_value_for_key(key, pipeline, _additional_data), do: pipeline |> Map.get(key)
end

View File

@ -0,0 +1,44 @@
defmodule MemexWeb.Components.StepContent do
@moduledoc """
Display the content for a step
"""
use MemexWeb, :component
alias Memex.Pipelines.Steps.Step
alias Phoenix.HTML
attr :step, Step, required: true
def step_content(assigns) do
~H"""
<div
id={"show-step-content-#{@step.id}"}
class="input input-primary h-32 min-h-32 inline-block"
phx-hook="MaintainAttrs"
phx-update="ignore"
readonly
phx-no-format
><p class="inline"><%= add_links_to_content(@step.content) %></p></div>
"""
end
defp add_links_to_content(content) do
Regex.replace(
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
content,
fn _whole_match, slug ->
link =
HTML.Link.link(
"[[#{slug}]]",
to: Routes.context_show_path(Endpoint, :show, slug),
class: "link inline",
data: [qa: "step-context-#{slug}"]
)
|> HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
"</p>#{link}<p class=\"inline\">"
end
)
|> HTML.raw()
end
end

View File

@ -20,7 +20,7 @@ defmodule MemexWeb.Components.Topbar do
navigate={Routes.live_path(Endpoint, HomeLive)} navigate={Routes.live_path(Endpoint, HomeLive)}
class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline" class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline"
> >
<%= gettext("memex") %> <%= gettext("memEx") %>
</.link> </.link>
<%= if @title_content do %> <%= if @title_content do %>
@ -65,7 +65,7 @@ defmodule MemexWeb.Components.Topbar do
<li class="mx-2 my-1 border-left border border-primary-700"></li> <li class="mx-2 my-1 border-left border border-primary-700"></li>
<%= if @current_user do %> <%= if @current_user do %>
<%= if @current_user.role == :admin do %> <%= if @current_user |> Accounts.is_already_admin?() do %>
<li class="mx-2 my-1"> <li class="mx-2 my-1">
<.link <.link
navigate={Routes.invite_index_path(Endpoint, :index)} navigate={Routes.invite_index_path(Endpoint, :index)}

View File

@ -2,7 +2,6 @@ defmodule MemexWeb.UserRegistrationController do
use MemexWeb, :controller use MemexWeb, :controller
import MemexWeb.Gettext import MemexWeb.Gettext
alias Memex.{Accounts, Invites} alias Memex.{Accounts, Invites}
alias Memex.Accounts.User
alias MemexWeb.HomeLive alias MemexWeb.HomeLive
def new(conn, %{"invite" => invite_token}) do def new(conn, %{"invite" => invite_token}) do
@ -30,7 +29,7 @@ defmodule MemexWeb.UserRegistrationController do
# renders new user registration page # renders new user registration page
defp render_new(conn, invite \\ nil) do defp render_new(conn, invite \\ nil) do
render(conn, "new.html", render(conn, "new.html",
changeset: Accounts.change_user_registration(%User{}), changeset: Accounts.change_user_registration(),
invite: invite, invite: invite,
page_title: gettext("register") page_title: gettext("register")
) )

View File

@ -4,8 +4,8 @@ defmodule MemexWeb.ContextLive.FormComponent do
alias Memex.Contexts alias Memex.Contexts
@impl true @impl true
def update(%{context: context} = assigns, socket) do def update(%{context: context, current_user: current_user} = assigns, socket) do
changeset = Contexts.change_context(context) changeset = Contexts.change_context(context, current_user)
{:ok, {:ok,
socket socket
@ -14,39 +14,52 @@ defmodule MemexWeb.ContextLive.FormComponent do
end end
@impl true @impl true
def handle_event("validate", %{"context" => context_params}, socket) do def handle_event(
"validate",
%{"context" => context_params},
%{assigns: %{context: context, current_user: current_user}} = socket
) do
changeset = changeset =
socket.assigns.context context
|> Contexts.change_context(context_params) |> Contexts.change_context(context_params, current_user)
|> Map.put(:action, :validate) |> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
end end
def handle_event("save", %{"context" => context_params}, socket) do def handle_event("save", %{"context" => context_params}, %{assigns: %{action: action}} = socket) do
save_context(socket, socket.assigns.action, context_params) save_context(socket, action, context_params)
end end
defp save_context(socket, :edit, context_params) do defp save_context(
case Contexts.update_context(socket.assigns.context, context_params) do %{assigns: %{context: context, return_to: return_to, current_user: current_user}} =
{:ok, _context} -> socket,
:edit,
context_params
) do
case Contexts.update_context(context, context_params, current_user) do
{:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "context updated successfully") |> put_flash(:info, gettext("%{slug} saved", slug: slug))
|> push_navigate(to: socket.assigns.return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
end end
end end
defp save_context(socket, :new, context_params) do defp save_context(
case Contexts.create_context(context_params) do %{assigns: %{return_to: return_to, current_user: current_user}} = socket,
{:ok, _context} -> :new,
context_params
) do
case Contexts.create_context(context_params, current_user) do
{:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "context created successfully") |> put_flash(:info, gettext("%{slug} created", slug: slug))
|> push_navigate(to: socket.assigns.return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)} {:noreply, assign(socket, changeset: changeset)}

View File

@ -1,6 +1,4 @@
<div> <div class="h-full flex flex-col justify-start items-stretch space-y-4">
<h2><%= @title %></h2>
<.form <.form
:let={f} :let={f}
for={@changeset} for={@changeset}
@ -8,27 +6,44 @@
phx-target={@myself} phx-target={@myself}
phx-change="validate" phx-change="validate"
phx-submit="save" phx-submit="save"
phx-debounce="300"
class="flex flex-col justify-start items-stretch space-y-4"
> >
<%= label(f, :title) %> <%= text_input(f, :slug,
<%= text_input(f, :title) %> class: "input input-primary",
<%= error_tag(f, :title) %> placeholder: gettext("slug")
) %>
<%= error_tag(f, :slug) %>
<%= label(f, :content) %> <%= textarea(f, :content,
<%= textarea(f, :content) %> id: "context-form-content",
class: "input input-primary h-64 min-h-64",
phx_hook: "MaintainAttrs",
phx_update: "ignore",
placeholder: gettext("use [[note-slug]] to link to a note")
) %>
<%= error_tag(f, :content) %> <%= error_tag(f, :content) %>
<%= label(f, :tag) %> <%= text_input(f, :tags_string,
<%= multiple_select(f, :tag, "Option 1": "option1", "Option 2": "option2") %> id: "tags-input",
<%= error_tag(f, :tag) %> class: "input input-primary",
placeholder: gettext("tag1,tag2"),
<%= label(f, :visibility) %> phx_update: "ignore",
<%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility), value: Contexts.get_tags_string(@changeset)
prompt: "Choose a value"
) %> ) %>
<%= error_tag(f, :visibility) %> <%= error_tag(f, :tags_string) %>
<div> <div class="flex justify-center items-stretch space-x-4">
<%= submit("Save", phx_disable_with: "Saving...") %> <%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
class: "grow input input-primary",
prompt: gettext("select privacy")
) %>
<%= submit(dgettext("actions", "save"),
phx_disable_with: gettext("saving..."),
class: "mx-auto btn btn-primary"
) %>
</div> </div>
<%= error_tag(f, :visibility) %>
</.form> </.form>
</div> </div>

View File

@ -1,46 +1,89 @@
defmodule MemexWeb.ContextLive.Index do defmodule MemexWeb.ContextLive.Index do
use MemexWeb, :live_view use MemexWeb, :live_view
alias Memex.{Accounts.User, Contexts, Contexts.Context}
alias Memex.Contexts
alias Memex.Contexts.Context
@impl true @impl true
def mount(%{"search" => search}, _session, socket) do
{:ok, socket |> assign(search: search) |> display_contexts()}
end
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
{:ok, assign(socket, :contexts, list_contexts())} {:ok, socket |> assign(search: nil) |> display_contexts()}
end end
@impl true @impl true
def handle_params(params, _url, socket) do def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)} {:noreply, apply_action(socket, live_action, params)}
end end
defp apply_action(socket, :edit, %{"id" => id}) do defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
%{slug: slug} = context = Contexts.get_context_by_slug(slug, current_user)
socket socket
|> assign(:page_title, "edit context") |> assign(page_title: gettext("edit %{slug}", slug: slug))
|> assign(:context, Contexts.get_context!(id)) |> assign(context: context)
end end
defp apply_action(socket, :new, _params) do defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
socket socket
|> assign(:page_title, "new context") |> assign(page_title: gettext("new context"))
|> assign(:context, %Context{}) |> assign(context: %Context{visibility: :private, user_id: current_user_id})
end end
defp apply_action(socket, :index, _params) do defp apply_action(socket, :index, _params) do
socket socket
|> assign(:page_title, "listing contexts") |> assign(page_title: gettext("contexts"))
|> assign(:context, nil) |> assign(search: nil)
|> assign(context: nil)
|> display_contexts()
end
defp apply_action(socket, :search, %{"search" => search}) do
socket
|> assign(page_title: gettext("contexts"))
|> assign(search: search)
|> assign(context: nil)
|> display_contexts()
end end
@impl true @impl true
def handle_event("delete", %{"id" => id}, socket) do def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
context = Contexts.get_context!(id) context = Contexts.get_context!(id, current_user)
{:ok, _} = Contexts.delete_context(context) {:ok, %{slug: slug}} = Contexts.delete_context(context, current_user)
{:noreply, assign(socket, :contexts, list_contexts())} socket =
socket
|> assign(contexts: Contexts.list_contexts(current_user))
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
{:noreply, socket}
end end
defp list_contexts do @impl true
Contexts.list_contexts() def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.context_index_path(Endpoint, :index))}
end end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
{:noreply,
socket |> push_patch(to: Routes.context_index_path(Endpoint, :search, search_term))}
end
defp display_contexts(%{assigns: %{current_user: current_user, search: search}} = socket)
when not (current_user |> is_nil()) do
socket |> assign(contexts: Contexts.list_contexts(search, current_user))
end
defp display_contexts(%{assigns: %{search: search}} = socket) do
socket |> assign(contexts: Contexts.list_public_contexts(search))
end
@spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Context.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -1,10 +1,71 @@
<h1>listing contexts</h1> <div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
<h1 class="text-xl">
<%= gettext("contexts") %>
</h1>
<.form
:let={f}
for={:search}
phx-change="search"
phx-submit="search"
class="self-stretch flex flex-col items-stretch"
>
<%= text_input(f, :search_term,
class: "input input-primary",
value: @search,
phx_debounce: 300,
placeholder: gettext("search")
) %>
</.form>
<%= if @contexts |> Enum.empty?() do %>
<h1 class="self-center text-primary-500">
<%= gettext("no contexts found") %>
</h1>
<% else %>
<.live_component
module={MemexWeb.Components.ContextsTableComponent}
id="contexts-index-table"
current_user={@current_user}
contexts={@contexts}
>
<:actions :let={context}>
<%= if is_owner?(context, @current_user) do %>
<.link
patch={Routes.context_index_path(@socket, :edit, context.slug)}
data-qa={"context-edit-#{context.id}"}
>
<%= dgettext("actions", "edit") %>
</.link>
<% end %>
<%= if is_owner_or_admin?(context, @current_user) do %>
<.link
href="#"
phx-click="delete"
phx-value-id={context.id}
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-context-#{context.id}"}
>
<%= dgettext("actions", "delete") %>
</.link>
<% end %>
</:actions>
</.live_component>
<% end %>
<%= if @current_user do %>
<.link patch={Routes.context_index_path(@socket, :new)} class="self-end btn btn-primary">
<%= dgettext("actions", "new context") %>
</.link>
<% end %>
</div>
<%= if @live_action in [:new, :edit] do %> <%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.context_index_path(@socket, :index)}> <.modal return_to={Routes.context_index_path(@socket, :index)}>
<.live_component <.live_component
module={MemexWeb.ContextLive.FormComponent} module={MemexWeb.ContextLive.FormComponent}
id={@context.id || :new} id={@context.id || :new}
current_user={@current_user}
title={@page_title} title={@page_title}
action={@live_action} action={@live_action}
context={@context} context={@context}
@ -12,55 +73,3 @@
/> />
</.modal> </.modal>
<% end %> <% end %>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>Tag</th>
<th>Visibility</th>
<th></th>
</tr>
</thead>
<tbody id="contexts">
<%= for context <- @contexts do %>
<tr id={"context-#{context.id}"}>
<td><%= context.title %></td>
<td><%= context.content %></td>
<td><%= context.tag %></td>
<td><%= context.visibility %></td>
<td>
<span>
<.link navigate={Routes.context_show_path(@socket, :show, context)}>
<%= dgettext("actions", "show") %>
</.link>
</span>
<span>
<.link patch={Routes.context_index_path(@socket, :edit, context)}>
<%= dgettext("actions", "edit") %>
</.link>
</span>
<span>
<.link
href="#"
phx-click="delete"
phx-value-id={context.id}
data-confirm={dgettext("prompts", "are you sure?")}
>
<%= dgettext("actions", "delete") %>
</.link>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span>
<.link patch={Routes.context_index_path(@socket, :new)}>
<%= dgettext("actions", "new context") %>
</.link>
</span>

View File

@ -1,7 +1,7 @@
defmodule MemexWeb.ContextLive.Show do defmodule MemexWeb.ContextLive.Show do
use MemexWeb, :live_view use MemexWeb, :live_view
import MemexWeb.Components.ContextContent
alias Memex.Contexts alias Memex.{Accounts.User, Contexts, Contexts.Context}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -9,13 +9,50 @@ defmodule MemexWeb.ContextLive.Show do
end end
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(
{:noreply, %{"slug" => slug},
socket _,
|> assign(:page_title, page_title(socket.assigns.live_action)) %{assigns: %{live_action: live_action, current_user: current_user}} = socket
|> assign(:context, Contexts.get_context!(id))} ) do
context =
case Contexts.get_context_by_slug(slug, current_user) do
nil -> raise MemexWeb.NotFoundError, gettext("%{slug} could not be found", slug: slug)
context -> context
end
socket =
socket
|> assign(:page_title, page_title(live_action, context))
|> assign(:context, context)
{:noreply, socket}
end end
defp page_title(:show), do: "show context" @impl true
defp page_title(:edit), do: "edit context" def handle_event(
"delete",
_params,
%{assigns: %{context: context, current_user: current_user}} = socket
) do
{:ok, %{slug: slug}} = Contexts.delete_context(context, current_user)
socket =
socket
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|> push_navigate(to: Routes.context_index_path(Endpoint, :index))
{:noreply, socket}
end
defp page_title(:show, %{slug: slug}), do: slug
defp page_title(:edit, %{slug: slug}), do: gettext("edit %{slug}", slug: slug)
@spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Context.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -1,48 +1,52 @@
<h1>show context</h1> <div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="text-xl">
<%= @context.slug %>
</h1>
<p><%= if @context.tags, do: @context.tags |> Enum.join(", ") %></p>
<.context_content context={@context} />
<p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
</p>
<div class="self-end flex space-x-4">
<.link class="btn btn-primary" navigate={Routes.context_index_path(@socket, :index)}>
<%= dgettext("actions", "back") %>
</.link>
<%= if is_owner?(@context, @current_user) do %>
<.link
class="btn btn-primary"
patch={Routes.context_show_path(@socket, :edit, @context.slug)}
>
<%= dgettext("actions", "edit") %>
</.link>
<% end %>
<%= if is_owner_or_admin?(@context, @current_user) do %>
<button
type="button"
class="btn btn-primary"
phx-click="delete"
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-context-#{@context.id}"}
>
<%= dgettext("actions", "delete") %>
</button>
<% end %>
</div>
</div>
<%= if @live_action in [:edit] do %> <%= if @live_action in [:edit] do %>
<.modal return_to={Routes.context_show_path(@socket, :show, @context)}> <.modal return_to={Routes.context_show_path(@socket, :show, @context.slug)}>
<.live_component <.live_component
module={MemexWeb.ContextLive.FormComponent} module={MemexWeb.ContextLive.FormComponent}
id={@context.id} id={@context.id}
current_user={@current_user}
title={@page_title} title={@page_title}
action={@live_action} action={@live_action}
context={@context} context={@context}
return_to={Routes.context_show_path(@socket, :show, @context)} return_to={Routes.context_show_path(@socket, :show, @context.slug)}
/> />
</.modal> </.modal>
<% end %> <% end %>
<ul>
<li>
<strong>Title:</strong>
<%= @context.title %>
</li>
<li>
<strong>Content:</strong>
<%= @context.content %>
</li>
<li>
<strong>Tag:</strong>
<%= @context.tag %>
</li>
<li>
<strong>Visibility:</strong>
<%= @context.visibility %>
</li>
</ul>
<span>
<.link patch={Routes.context_show_path(@socket, :edit, @context)} class="button">
<%= dgettext("actions", "edit") %>
</.link>
</span>
|
<span>
<.link navigate={Routes.context_index_path(@socket, :index)}>
<%= dgettext("actions", "Back") %>
</.link>
</span>

View File

@ -0,0 +1,12 @@
defmodule MemexWeb.FaqLive do
@moduledoc """
Liveview for the faq page
"""
use MemexWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket |> assign(page_title: gettext("faq"))}
end
end

View File

@ -0,0 +1,124 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-8 text-center max-w-3xl">
<h1 class="title text-primary-400 text-2xl">
<%= gettext("faq") %>
</h1>
<hr class="hr" />
<ul class="flex flex-col justify-center items-stretch space-y-8">
<li class="flex flex-col justify-center items-stretch space-y-2">
<b class="whitespace-nowrap">
<%= gettext("what is this?") %>
</b>
<p>
<%= gettext(
"this is a memex, used to document not just your notes, but also your perspectives and processes."
) %>
</p>
<p>
<%= gettext("some things that this memex is very loosely inspired by:") %>
</p>
<ul class="list-disc flex flex-col justify-center items-center space-y-2">
<li>
<.link
href="https://en.wikipedia.org/wiki/Memex"
class="flex flex-row justify-center items-center space-x-2 link"
target="_blank"
rel="noopener noreferrer"
>
<%= gettext("memex") %>
</.link>
</li>
<li>
<.link
href="https://en.wikipedia.org/wiki/Zettelkasten"
class="flex flex-row justify-center items-center space-x-2 link"
target="_blank"
rel="noopener noreferrer"
>
<%= gettext("zettelkasten") %>
</.link>
</li>
<li>
<.link
href="https://en.wikipedia.org/wiki/Org-mode"
class="flex flex-row justify-center items-center space-x-2 link"
target="_blank"
rel="noopener noreferrer"
>
<%= gettext("org-mode") %>
</.link>
</li>
</ul>
</li>
<li class="flex flex-col justify-center items-stretch space-y-2">
<b class="whitespace-nowrap">
<%= gettext("why split up into notes, contexts and pipelines?") %>
</b>
<p>
<%= gettext(
"i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
) %>
</p>
<p>
<%= gettext(
"i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
) %>
</p>
<p>
<%= gettext(
"finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
) %>
</p>
</li>
<li class="flex flex-col justify-center items-stretch space-y-2">
<b class="whitespace-nowrap">
<%= gettext("what should my notes be like?") %>
</b>
<p>
<%= gettext(
"in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
) %>
</p>
<p>
<%= gettext(
"spoons? probably not. a particular brand of spoons that you really like? why not :)"
) %>
</p>
</li>
<li class="flex flex-col justify-center items-stretch space-y-2">
<b class="whitespace-nowrap">
<%= gettext("what should my contexts be like?") %>
</b>
<p>
<%= gettext("in my opinion, contexts should be like single-topic blog posts.") %>
</p>
<p>
<%= gettext(
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
) %>
</p>
</li>
<li class="flex flex-col justify-center items-stretch space-y-2">
<b class="whitespace-nowrap">
<%= gettext("what should my pipelines be like?") %>
</b>
<p>
<%= gettext(
"in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
) %>
</p>
<p>
<%= gettext(
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
) %>
</p>
</li>
</ul>
</div>

View File

@ -5,41 +5,11 @@ defmodule MemexWeb.HomeLive do
use MemexWeb, :live_view use MemexWeb, :live_view
alias Memex.Accounts alias Memex.Accounts
alias MemexWeb.{Endpoint, FaqLive}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
admins = Accounts.list_users_by_role(:admin) admins = Accounts.list_users_by_role(:admin)
{:ok, socket |> assign(page_title: gettext("Home"), query: "", results: %{}, admins: admins)} {:ok, socket |> assign(page_title: gettext("home"), admins: admins)}
end
@impl true
def handle_event("suggest", %{"q" => query}, socket) do
{:noreply, socket |> assign(results: search(query), query: query)}
end
@impl true
def handle_event("search", %{"q" => query}, socket) do
case search(query) do
%{^query => vsn} ->
{:noreply, socket |> redirect(external: "https://hexdocs.pm/#{query}/#{vsn}")}
_ ->
{:noreply,
socket
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|> assign(results: %{}, query: query)}
end
end
defp search(query) do
if not MemexWeb.Endpoint.config(:code_reloader) do
raise "action disabled when not in development"
end
for {app, desc, vsn} <- Application.started_applications(),
app = to_string(app),
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
into: %{},
do: {app, vsn}
end end
end end

View File

@ -1,13 +1,12 @@
<div class="flex flex-col justify-center items-center text-center space-y-4"> <div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="title text-primary-400 text-2xl"> <h1 class="title text-primary-400 text-2xl text-center">
<%= gettext("memex") %> <%= gettext("memEx") %>
</h1> </h1>
<hr class="hr" /> <hr class="hr" />
<ul class="flex flex-col space-y-4 text-center"> <ul class="flex flex-col space-y-4 text-center">
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("notes:") %> <%= gettext("notes:") %>
</b> </b>
@ -16,8 +15,7 @@
</p> </p>
</li> </li>
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("contexts:") %> <%= gettext("contexts:") %>
</b> </b>
@ -26,8 +24,7 @@
</p> </p>
</li> </li>
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("pipelines:") %> <%= gettext("pipelines:") %>
</b> </b>
@ -35,6 +32,15 @@
<%= gettext("document your processes, attaching contexts to each step") %> <%= gettext("document your processes, attaching contexts to each step") %>
</p> </p>
</li> </li>
<li class="flex flex-col justify-center items-center space-y-2">
<.link
navigate={Routes.live_path(Endpoint, FaqLive)}
class="link title text-primary-400 text-lg"
>
<%= gettext("read more on how to use %{name}", name: "memEx") %>
</.link>
</li>
</ul> </ul>
<hr class="hr" /> <hr class="hr" />
@ -44,8 +50,7 @@
<%= gettext("features") %> <%= gettext("features") %>
</h2> </h2>
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("multi-user:") %> <%= gettext("multi-user:") %>
</b> </b>
@ -54,8 +59,7 @@
</p> </p>
</li> </li>
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("privacy:") %> <%= gettext("privacy:") %>
</b> </b>
@ -64,8 +68,7 @@
</p> </p>
</li> </li>
<li class="flex flex-col justify-center items-center <li class="flex flex-col justify-center items-center space-y-2">
space-y-2">
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
<%= gettext("convenient:") %> <%= gettext("convenient:") %>
</b> </b>
@ -88,16 +91,13 @@
</b> </b>
<p> <p>
<%= if @admins |> Enum.empty?() do %> <%= if @admins |> Enum.empty?() do %>
<.link <.link href={Routes.user_registration_path(Endpoint, :new)} class="link">
href={Routes.user_registration_path(MemexWeb.Endpoint, :new)} <%= dgettext("prompts", "register to setup %{name}", name: "memEx") %>
class="hover:underline"
>
<%= dgettext("prompts", "register to setup %{name}", name: "memex") %>
</.link> </.link>
<% else %> <% else %>
<div class="flex flex-wrap justify-center space-x-2"> <div class="flex flex-wrap justify-center space-x-2">
<%= for admin <- @admins do %> <%= for admin <- @admins do %>
<a class="hover:underline" href={"mailto:#{admin.email}"}> <a class="link" href={"mailto:#{admin.email}"}>
<%= admin.email %> <%= admin.email %>
</a> </a>
<% end %> <% end %>
@ -109,7 +109,7 @@
<li class="flex flex-row justify-center space-x-2"> <li class="flex flex-row justify-center space-x-2">
<b><%= gettext("registration:") %></b> <b><%= gettext("registration:") %></b>
<p> <p>
<%= Application.get_env(:memex, MemexWeb.Endpoint)[:registration] <%= Application.get_env(:memex, Endpoint)[:registration]
|> case do |> case do
"public" -> gettext("public signups") "public" -> gettext("public signups")
_ -> gettext("invite only") _ -> gettext("invite only")
@ -121,7 +121,7 @@
<b><%= gettext("version:") %></b> <b><%= gettext("version:") %></b>
<.link <.link
href="https://gitea.bubbletea.dev/shibao/memex/src/branch/stable/CHANGELOG.md" href="https://gitea.bubbletea.dev/shibao/memex/src/branch/stable/CHANGELOG.md"
class="flex flex-row justify-center items-center space-x-2 hover:underline" class="flex flex-row justify-center items-center space-x-2 link"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -141,7 +141,7 @@
<li class="flex flex-col justify-center space-x-2"> <li class="flex flex-col justify-center space-x-2">
<.link <.link
href="https://gitea.bubbletea.dev/shibao/memex" href="https://gitea.bubbletea.dev/shibao/memex"
class="flex flex-row justify-center items-center space-x-2 hover:underline" class="flex flex-row justify-center items-center space-x-2 link"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -152,7 +152,7 @@
<li class="flex flex-col justify-center space-x-2"> <li class="flex flex-col justify-center space-x-2">
<.link <.link
href="https://weblate.bubbletea.dev/engage/memex" href="https://weblate.bubbletea.dev/engage/memex"
class="flex flex-row justify-center items-center space-x-2 hover:underline" class="flex flex-row justify-center items-center space-x-2 link"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -163,7 +163,7 @@
<li class="flex flex-col justify-center space-x-2"> <li class="flex flex-col justify-center space-x-2">
<.link <.link
href="https://gitea.bubbletea.dev/shibao/memex/issues/new" href="https://gitea.bubbletea.dev/shibao/memex/issues/new"
class="flex flex-row justify-center items-center space-x-2 hover:underline" class="flex flex-row justify-center items-center space-x-2 link"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@ -37,10 +37,10 @@ defmodule MemexWeb.NoteLive.FormComponent do
note_params note_params
) do ) do
case Notes.update_note(note, note_params, current_user) do case Notes.update_note(note, note_params, current_user) do
{:ok, %{title: title}} -> {:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, gettext("%{title} saved", title: title)) |> put_flash(:info, gettext("%{slug} saved", slug: slug))
|> push_navigate(to: return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
@ -54,10 +54,10 @@ defmodule MemexWeb.NoteLive.FormComponent do
note_params note_params
) do ) do
case Notes.create_note(note_params, current_user) do case Notes.create_note(note_params, current_user) do
{:ok, %{title: title}} -> {:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, gettext("%{title} created", title: title)) |> put_flash(:info, gettext("%{slug} created", slug: slug))
|> push_navigate(to: return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->

View File

@ -9,11 +9,11 @@
phx-debounce="300" phx-debounce="300"
class="flex flex-col justify-start items-stretch space-y-4" class="flex flex-col justify-start items-stretch space-y-4"
> >
<%= text_input(f, :title, <%= text_input(f, :slug,
class: "input input-primary", class: "input input-primary",
placeholder: gettext("title") placeholder: gettext("slug")
) %> ) %>
<%= error_tag(f, :title) %> <%= error_tag(f, :slug) %>
<%= textarea(f, :content, <%= textarea(f, :content,
id: "note-form-content", id: "note-form-content",

View File

@ -1,6 +1,6 @@
defmodule MemexWeb.NoteLive.Index do defmodule MemexWeb.NoteLive.Index do
use MemexWeb, :live_view use MemexWeb, :live_view
alias Memex.{Notes, Notes.Note} alias Memex.{Accounts.User, Notes, Notes.Note}
@impl true @impl true
def mount(%{"search" => search}, _session, socket) do def mount(%{"search" => search}, _session, socket) do
@ -16,23 +16,23 @@ defmodule MemexWeb.NoteLive.Index do
{:noreply, apply_action(socket, live_action, params)} {:noreply, apply_action(socket, live_action, params)}
end end
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
%{title: title} = note = Notes.get_note!(id, current_user) %{slug: slug} = note = Notes.get_note_by_slug(slug, current_user)
socket socket
|> assign(page_title: gettext("edit %{title}", title: title)) |> assign(page_title: gettext("edit %{slug}", slug: slug))
|> assign(note: note) |> assign(note: note)
end end
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
socket socket
|> assign(page_title: "new note") |> assign(page_title: gettext("new note"))
|> assign(note: %Note{user_id: current_user_id}) |> assign(note: %Note{visibility: :private, user_id: current_user_id})
end end
defp apply_action(socket, :index, _params) do defp apply_action(socket, :index, _params) do
socket socket
|> assign(page_title: "notes") |> assign(page_title: gettext("notes"))
|> assign(search: nil) |> assign(search: nil)
|> assign(note: nil) |> assign(note: nil)
|> display_notes() |> display_notes()
@ -40,7 +40,7 @@ defmodule MemexWeb.NoteLive.Index do
defp apply_action(socket, :search, %{"search" => search}) do defp apply_action(socket, :search, %{"search" => search}) do
socket socket
|> assign(page_title: "notes") |> assign(page_title: gettext("notes"))
|> assign(search: search) |> assign(search: search)
|> assign(note: nil) |> assign(note: nil)
|> display_notes() |> display_notes()
@ -48,13 +48,13 @@ defmodule MemexWeb.NoteLive.Index do
@impl true @impl true
def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
%{title: title} = note = Notes.get_note!(id, current_user) note = Notes.get_note!(id, current_user)
{:ok, _} = Notes.delete_note(note, current_user) {:ok, %{slug: slug}} = Notes.delete_note(note, current_user)
socket = socket =
socket socket
|> assign(notes: Notes.list_notes(current_user)) |> assign(notes: Notes.list_notes(current_user))
|> put_flash(:info, gettext("%{title} deleted", title: title)) |> put_flash(:info, gettext("%{slug} deleted", slug: slug))
{:noreply, socket} {:noreply, socket}
end end
@ -76,4 +76,13 @@ defmodule MemexWeb.NoteLive.Index do
defp display_notes(%{assigns: %{search: search}} = socket) do defp display_notes(%{assigns: %{search: search}} = socket) do
socket |> assign(notes: Notes.list_public_notes(search)) socket |> assign(notes: Notes.list_public_notes(search))
end end
@spec is_owner_or_admin?(Note.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Note.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -8,10 +8,14 @@
for={:search} for={:search}
phx-change="search" phx-change="search"
phx-submit="search" phx-submit="search"
phx-debounce="500"
class="self-stretch flex flex-col items-stretch" class="self-stretch flex flex-col items-stretch"
> >
<%= text_input(f, :search_term, class: "input input-primary", value: @search) %> <%= text_input(f, :search_term,
class: "input input-primary",
value: @search,
phx_debounce: 300,
placeholder: gettext("search")
) %>
</.form> </.form>
<%= if @notes |> Enum.empty?() do %> <%= if @notes |> Enum.empty?() do %>
@ -26,13 +30,15 @@
notes={@notes} notes={@notes}
> >
<:actions :let={note}> <:actions :let={note}>
<%= if @current_user do %> <%= if is_owner?(note, @current_user) do %>
<.link <.link
patch={Routes.note_index_path(@socket, :edit, note)} patch={Routes.note_index_path(@socket, :edit, note.slug)}
data-qa={"note-edit-#{note.id}"} data-qa={"note-edit-#{note.id}"}
> >
<%= dgettext("actions", "edit") %> <%= dgettext("actions", "edit") %>
</.link> </.link>
<% end %>
<%= if is_owner_or_admin?(note, @current_user) do %>
<.link <.link
href="#" href="#"
phx-click="delete" phx-click="delete"

View File

@ -1,7 +1,7 @@
defmodule MemexWeb.NoteLive.Show do defmodule MemexWeb.NoteLive.Show do
use MemexWeb, :live_view use MemexWeb, :live_view
alias Memex.Notes alias Memex.{Accounts.User, Notes, Notes.Note}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -10,16 +10,49 @@ defmodule MemexWeb.NoteLive.Show do
@impl true @impl true
def handle_params( def handle_params(
%{"id" => id}, %{"slug" => slug},
_, _,
%{assigns: %{live_action: live_action, current_user: current_user}} = socket %{assigns: %{live_action: live_action, current_user: current_user}} = socket
) do ) do
{:noreply, note =
socket case Notes.get_note_by_slug(slug, current_user) do
|> assign(:page_title, page_title(live_action)) nil -> raise MemexWeb.NotFoundError, gettext("%{slug} could not be found", slug: slug)
|> assign(:note, Notes.get_note!(id, current_user))} note -> note
end
socket =
socket
|> assign(:page_title, page_title(live_action, note))
|> assign(:note, note)
{:noreply, socket}
end end
defp page_title(:show), do: "show note" @impl true
defp page_title(:edit), do: "edit note" def handle_event(
"delete",
_params,
%{assigns: %{note: note, current_user: current_user}} = socket
) do
{:ok, %{slug: slug}} = Notes.delete_note(note, current_user)
socket =
socket
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|> push_navigate(to: Routes.note_index_path(Endpoint, :index))
{:noreply, socket}
end
defp page_title(:show, %{slug: slug}), do: slug
defp page_title(:edit, %{slug: slug}), do: gettext("edit %{slug}", slug: slug)
@spec is_owner_or_admin?(Note.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Note.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -1,6 +1,6 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl"> <div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="text-xl"> <h1 class="text-xl">
<%= @note.title %> <%= @note.slug %>
</h1> </h1>
<p><%= if @note.tags, do: @note.tags |> Enum.join(", ") %></p> <p><%= if @note.tags, do: @note.tags |> Enum.join(", ") %></p>
@ -19,19 +19,30 @@
</p> </p>
<div class="self-end flex space-x-4"> <div class="self-end flex space-x-4">
<.link class="btn btn-primary" patch={Routes.note_index_path(@socket, :index)}> <.link class="btn btn-primary" navigate={Routes.note_index_path(@socket, :index)}>
<%= dgettext("actions", "Back") %> <%= dgettext("actions", "back") %>
</.link> </.link>
<%= if @current_user do %> <%= if is_owner?(@note, @current_user) do %>
<.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note)}> <.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note.slug)}>
<%= dgettext("actions", "edit") %> <%= dgettext("actions", "edit") %>
</.link> </.link>
<% end %> <% end %>
<%= if is_owner_or_admin?(@note, @current_user) do %>
<button
type="button"
class="btn btn-primary"
phx-click="delete"
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-note-#{@note.id}"}
>
<%= dgettext("actions", "delete") %>
</button>
<% end %>
</div> </div>
</div> </div>
<%= if @live_action in [:edit] do %> <%= if @live_action in [:edit] do %>
<.modal return_to={Routes.note_show_path(@socket, :show, @note)}> <.modal return_to={Routes.note_show_path(@socket, :show, @note.slug)}>
<.live_component <.live_component
module={MemexWeb.NoteLive.FormComponent} module={MemexWeb.NoteLive.FormComponent}
id={@note.id} id={@note.id}
@ -39,7 +50,7 @@
title={@page_title} title={@page_title}
action={@live_action} action={@live_action}
note={@note} note={@note}
return_to={Routes.note_show_path(@socket, :show, @note)} return_to={Routes.note_show_path(@socket, :show, @note.slug)}
/> />
</.modal> </.modal>
<% end %> <% end %>

View File

@ -4,8 +4,8 @@ defmodule MemexWeb.PipelineLive.FormComponent do
alias Memex.Pipelines alias Memex.Pipelines
@impl true @impl true
def update(%{pipeline: pipeline} = assigns, socket) do def update(%{pipeline: pipeline, current_user: current_user} = assigns, socket) do
changeset = Pipelines.change_pipeline(pipeline) changeset = Pipelines.change_pipeline(pipeline, current_user)
{:ok, {:ok,
socket socket
@ -14,39 +14,56 @@ defmodule MemexWeb.PipelineLive.FormComponent do
end end
@impl true @impl true
def handle_event("validate", %{"pipeline" => pipeline_params}, socket) do def handle_event(
"validate",
%{"pipeline" => pipeline_params},
%{assigns: %{pipeline: pipeline, current_user: current_user}} = socket
) do
changeset = changeset =
socket.assigns.pipeline pipeline
|> Pipelines.change_pipeline(pipeline_params) |> Pipelines.change_pipeline(pipeline_params, current_user)
|> Map.put(:action, :validate) |> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
end end
def handle_event("save", %{"pipeline" => pipeline_params}, socket) do def handle_event(
save_pipeline(socket, socket.assigns.action, pipeline_params) "save",
%{"pipeline" => pipeline_params},
%{assigns: %{action: action}} = socket
) do
save_pipeline(socket, action, pipeline_params)
end end
defp save_pipeline(socket, :edit, pipeline_params) do defp save_pipeline(
case Pipelines.update_pipeline(socket.assigns.pipeline, pipeline_params) do %{assigns: %{pipeline: pipeline, return_to: return_to, current_user: current_user}} =
{:ok, _pipeline} -> socket,
:edit,
pipeline_params
) do
case Pipelines.update_pipeline(pipeline, pipeline_params, current_user) do
{:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "pipeline updated successfully") |> put_flash(:info, gettext("%{slug} saved", slug: slug))
|> push_navigate(to: socket.assigns.return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
end end
end end
defp save_pipeline(socket, :new, pipeline_params) do defp save_pipeline(
case Pipelines.create_pipeline(pipeline_params) do %{assigns: %{return_to: return_to, current_user: current_user}} = socket,
{:ok, _pipeline} -> :new,
pipeline_params
) do
case Pipelines.create_pipeline(pipeline_params, current_user) do
{:ok, %{slug: slug}} ->
{:noreply, {:noreply,
socket socket
|> put_flash(:info, "pipeline created successfully") |> put_flash(:info, gettext("%{slug} created", slug: slug))
|> push_navigate(to: socket.assigns.return_to)} |> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)} {:noreply, assign(socket, changeset: changeset)}

View File

@ -1,6 +1,4 @@
<div> <div class="h-full flex flex-col justify-start items-stretch space-y-4">
<h2><%= @title %></h2>
<.form <.form
:let={f} :let={f}
for={@changeset} for={@changeset}
@ -8,23 +6,44 @@
phx-target={@myself} phx-target={@myself}
phx-change="validate" phx-change="validate"
phx-submit="save" phx-submit="save"
phx-debounce="300"
class="flex flex-col justify-start items-stretch space-y-4"
> >
<%= label(f, :title) %> <%= text_input(f, :slug,
<%= text_input(f, :title) %> class: "input input-primary",
<%= error_tag(f, :title) %> placeholder: gettext("slug")
) %>
<%= error_tag(f, :slug) %>
<%= label(f, :description) %> <%= textarea(f, :description,
<%= textarea(f, :description) %> id: "pipeline-form-description",
class: "input input-primary h-64 min-h-64",
phx_hook: "MaintainAttrs",
phx_update: "ignore",
placeholder: gettext("description")
) %>
<%= error_tag(f, :description) %> <%= error_tag(f, :description) %>
<%= label(f, :visibility) %> <%= text_input(f, :tags_string,
<%= select(f, :visibility, Ecto.Enum.values(Memex.Pipelines.Pipeline, :visibility), id: "tags-input",
prompt: "Choose a value" class: "input input-primary",
placeholder: gettext("tag1,tag2"),
phx_update: "ignore",
value: Pipelines.get_tags_string(@changeset)
) %> ) %>
<%= error_tag(f, :visibility) %> <%= error_tag(f, :tags_string) %>
<div> <div class="flex justify-center items-stretch space-x-4">
<%= submit("Save", phx_disable_with: "Saving...") %> <%= select(f, :visibility, Ecto.Enum.values(Memex.Pipelines.Pipeline, :visibility),
class: "grow input input-primary",
prompt: gettext("select privacy")
) %>
<%= submit(dgettext("actions", "save"),
phx_disable_with: gettext("saving..."),
class: "mx-auto btn btn-primary"
) %>
</div> </div>
<%= error_tag(f, :visibility) %>
</.form> </.form>
</div> </div>

View File

@ -1,46 +1,89 @@
defmodule MemexWeb.PipelineLive.Index do defmodule MemexWeb.PipelineLive.Index do
use MemexWeb, :live_view use MemexWeb, :live_view
alias Memex.{Accounts.User, Pipelines, Pipelines.Pipeline}
alias Memex.Pipelines
alias Memex.Pipelines.Pipeline
@impl true @impl true
def mount(%{"search" => search}, _session, socket) do
{:ok, socket |> assign(search: search) |> display_pipelines()}
end
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
{:ok, assign(socket, :pipelines, list_pipelines())} {:ok, socket |> assign(search: nil) |> display_pipelines()}
end end
@impl true @impl true
def handle_params(params, _url, socket) do def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)} {:noreply, apply_action(socket, live_action, params)}
end end
defp apply_action(socket, :edit, %{"id" => id}) do defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
%{slug: slug} = pipeline = Pipelines.get_pipeline_by_slug(slug, current_user)
socket socket
|> assign(:page_title, "edit pipeline") |> assign(page_title: gettext("edit %{slug}", slug: slug))
|> assign(:pipeline, Pipelines.get_pipeline!(id)) |> assign(pipeline: pipeline)
end end
defp apply_action(socket, :new, _params) do defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
socket socket
|> assign(:page_title, "new Pipeline") |> assign(page_title: gettext("new pipeline"))
|> assign(:pipeline, %Pipeline{}) |> assign(pipeline: %Pipeline{visibility: :private, user_id: current_user_id})
end end
defp apply_action(socket, :index, _params) do defp apply_action(socket, :index, _params) do
socket socket
|> assign(:page_title, "listing pipelines") |> assign(page_title: gettext("pipelines"))
|> assign(:pipeline, nil) |> assign(search: nil)
|> assign(pipeline: nil)
|> display_pipelines()
end
defp apply_action(socket, :search, %{"search" => search}) do
socket
|> assign(page_title: gettext("pipelines"))
|> assign(search: search)
|> assign(pipeline: nil)
|> display_pipelines()
end end
@impl true @impl true
def handle_event("delete", %{"id" => id}, socket) do def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do
pipeline = Pipelines.get_pipeline!(id) pipeline = Pipelines.get_pipeline!(id, current_user)
{:ok, _} = Pipelines.delete_pipeline(pipeline) {:ok, %{slug: slug}} = Pipelines.delete_pipeline(pipeline, current_user)
{:noreply, assign(socket, :pipelines, list_pipelines())} socket =
socket
|> assign(pipelines: Pipelines.list_pipelines(current_user))
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
{:noreply, socket}
end end
defp list_pipelines do @impl true
Pipelines.list_pipelines() def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
{:noreply, socket |> push_patch(to: Routes.pipeline_index_path(Endpoint, :index))}
end end
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
{:noreply,
socket |> push_patch(to: Routes.pipeline_index_path(Endpoint, :search, search_term))}
end
defp display_pipelines(%{assigns: %{current_user: current_user, search: search}} = socket)
when not (current_user |> is_nil()) do
socket |> assign(pipelines: Pipelines.list_pipelines(search, current_user))
end
defp display_pipelines(%{assigns: %{search: search}} = socket) do
socket |> assign(pipelines: Pipelines.list_public_pipelines(search))
end
@spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Pipeline.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -1,10 +1,71 @@
<h1>listing pipelines</h1> <div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
<h1 class="text-xl">
<%= gettext("pipelines") %>
</h1>
<.form
:let={f}
for={:search}
phx-change="search"
phx-submit="search"
class="self-stretch flex flex-col items-stretch"
>
<%= text_input(f, :search_term,
class: "input input-primary",
value: @search,
phx_debounce: 300,
placeholder: gettext("search")
) %>
</.form>
<%= if @pipelines |> Enum.empty?() do %>
<h1 class="self-center text-primary-500">
<%= gettext("no pipelines found") %>
</h1>
<% else %>
<.live_component
module={MemexWeb.Components.PipelinesTableComponent}
id="pipelines-index-table"
current_user={@current_user}
pipelines={@pipelines}
>
<:actions :let={pipeline}>
<%= if is_owner?(pipeline, @current_user) do %>
<.link
patch={Routes.pipeline_index_path(@socket, :edit, pipeline.slug)}
data-qa={"pipeline-edit-#{pipeline.id}"}
>
<%= dgettext("actions", "edit") %>
</.link>
<% end %>
<%= if is_owner_or_admin?(pipeline, @current_user) do %>
<.link
href="#"
phx-click="delete"
phx-value-id={pipeline.id}
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-pipeline-#{pipeline.id}"}
>
<%= dgettext("actions", "delete") %>
</.link>
<% end %>
</:actions>
</.live_component>
<% end %>
<%= if @current_user do %>
<.link patch={Routes.pipeline_index_path(@socket, :new)} class="self-end btn btn-primary">
<%= dgettext("actions", "new pipeline") %>
</.link>
<% end %>
</div>
<%= if @live_action in [:new, :edit] do %> <%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.pipeline_index_path(@socket, :index)}> <.modal return_to={Routes.pipeline_index_path(@socket, :index)}>
<.live_component <.live_component
module={MemexWeb.PipelineLive.FormComponent} module={MemexWeb.PipelineLive.FormComponent}
id={@pipeline.id || :new} id={@pipeline.id || :new}
current_user={@current_user}
title={@page_title} title={@page_title}
action={@live_action} action={@live_action}
pipeline={@pipeline} pipeline={@pipeline}
@ -12,53 +73,3 @@
/> />
</.modal> </.modal>
<% end %> <% end %>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Visibility</th>
<th></th>
</tr>
</thead>
<tbody id="pipelines">
<%= for pipeline <- @pipelines do %>
<tr id={"pipeline-#{pipeline.id}"}>
<td><%= pipeline.title %></td>
<td><%= pipeline.description %></td>
<td><%= pipeline.visibility %></td>
<td>
<span>
<.link navigate={Routes.pipeline_show_path(@socket, :show, pipeline)}>
<%= dgettext("actions", "show") %>
</.link>
</span>
<span>
<.link patch={Routes.pipeline_index_path(@socket, :edit, pipeline)}>
<%= dgettext("actions", "edit") %>
</.link>
</span>
<span>
<.link
href="#"
phx-click="delete"
phx-value-id={pipeline.id}
data-confirm={dgettext("prompts", "are you sure?")}
>
<%= dgettext("actions", "delete") %>
</.link>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span>
<.link patch={Routes.pipeline_index_path(@socket, :new)}>
<%= dgettext("actions", "new pipeline") %>
</.link>
</span>

View File

@ -1,7 +1,8 @@
defmodule MemexWeb.PipelineLive.Show do defmodule MemexWeb.PipelineLive.Show do
use MemexWeb, :live_view use MemexWeb, :live_view
import MemexWeb.Components.StepContent
alias Memex.Pipelines alias Memex.{Accounts.User, Pipelines}
alias Memex.Pipelines.{Pipeline, Steps, Steps.Step}
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -9,13 +10,128 @@ defmodule MemexWeb.PipelineLive.Show do
end end
@impl true @impl true
def handle_params(%{"id" => id}, _, socket) do def handle_params(
{:noreply, %{"slug" => slug} = params,
socket _url,
|> assign(:page_title, page_title(socket.assigns.live_action)) %{assigns: %{current_user: current_user, live_action: live_action}} = socket
|> assign(:pipeline, Pipelines.get_pipeline!(id))} ) do
pipeline =
case Pipelines.get_pipeline_by_slug(slug, current_user) do
nil -> raise MemexWeb.NotFoundError, gettext("%{slug} could not be found", slug: slug)
pipeline -> pipeline
end
socket =
socket
|> assign(:page_title, page_title(live_action, pipeline))
|> assign(:pipeline, pipeline)
|> assign(:steps, pipeline |> Steps.list_steps(current_user))
|> apply_action(live_action, params)
{:noreply, socket}
end end
defp page_title(:show), do: "show pipeline" defp apply_action(socket, live_action, _params) when live_action in [:show, :edit] do
defp page_title(:edit), do: "edit pipeline" socket
end
defp apply_action(
%{
assigns: %{
steps: steps,
pipeline: %{id: pipeline_id},
current_user: %{id: current_user_id}
}
} = socket,
:add_step,
_params
) do
socket
|> assign(
step: %Step{
position: steps |> Enum.count(),
pipeline_id: pipeline_id,
user_id: current_user_id
}
)
end
defp apply_action(
%{assigns: %{current_user: current_user}} = socket,
:edit_step,
%{"step_id" => step_id}
) do
socket |> assign(step: step_id |> Steps.get_step!(current_user))
end
@impl true
def handle_event(
"delete",
_params,
%{assigns: %{pipeline: pipeline, current_user: current_user}} = socket
) do
{:ok, %{slug: slug}} = Pipelines.delete_pipeline(pipeline, current_user)
socket =
socket
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|> push_navigate(to: Routes.pipeline_index_path(Endpoint, :index))
{:noreply, socket}
end
@impl true
def handle_event(
"delete_step",
%{"step-id" => step_id},
%{assigns: %{pipeline: %{slug: pipeline_slug}, current_user: current_user}} = socket
) do
{:ok, %{title: title}} =
step_id
|> Steps.get_step!(current_user)
|> Steps.delete_step(current_user)
socket =
socket
|> put_flash(:info, gettext("%{title} deleted", title: title))
|> push_patch(to: Routes.pipeline_show_path(Endpoint, :show, pipeline_slug))
{:noreply, socket}
end
@impl true
def handle_event(
"reorder_step",
%{"step-id" => step_id, "direction" => direction},
%{assigns: %{pipeline: %{slug: pipeline_slug}, current_user: current_user}} = socket
) do
direction = if direction == "up", do: :up, else: :down
{:ok, _step} =
step_id
|> Steps.get_step!(current_user)
|> Steps.reorder_step(direction, current_user)
socket =
socket
|> push_patch(to: Routes.pipeline_show_path(Endpoint, :show, pipeline_slug))
{:noreply, socket}
end
defp page_title(:show, %{slug: slug}), do: slug
defp page_title(live_action, %{slug: slug}) when live_action in [:edit, :edit_step],
do: gettext("edit %{slug}", slug: slug)
defp page_title(:add_step, %{slug: slug}), do: gettext("add step to %{slug}", slug: slug)
@spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean()
defp is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner_or_admin?(_context, %{role: :admin}), do: true
defp is_owner_or_admin?(_context, _other_user), do: false
@spec is_owner?(Pipeline.t(), User.t()) :: boolean()
defp is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
defp is_owner?(_context, _other_user), do: false
end end

View File

@ -1,43 +1,174 @@
<h1>show pipeline</h1> <div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="text-xl">
<%= @pipeline.slug %>
</h1>
<%= if @live_action in [:edit] do %> <p><%= if @pipeline.tags, do: @pipeline.tags |> Enum.join(", ") %></p>
<.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)}>
<.live_component <%= if @pipeline.description do %>
module={MemexWeb.PipelineLive.FormComponent} <textarea
id={@pipeline.id} id="show-pipeline-description"
title={@page_title} class="input input-primary h-32 min-h-32"
action={@live_action} phx-hook="MaintainAttrs"
pipeline={@pipeline} phx-update="ignore"
return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)} readonly
/> phx-no-format
</.modal> ><%= @pipeline.description %></textarea>
<% end %>
<p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @pipeline.visibility) %>
</p>
<div class="pb-4 self-end flex space-x-4">
<.link class="btn btn-primary" navigate={Routes.pipeline_index_path(@socket, :index)}>
<%= dgettext("actions", "back") %>
</.link>
<%= if is_owner?(@pipeline, @current_user) do %>
<.link
class="btn btn-primary"
patch={Routes.pipeline_show_path(@socket, :edit, @pipeline.slug)}
>
<%= dgettext("actions", "edit") %>
</.link>
<% end %>
<%= if is_owner_or_admin?(@pipeline, @current_user) do %>
<button
type="button"
class="btn btn-primary"
phx-click="delete"
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-pipeline-#{@pipeline.id}"}
>
<%= dgettext("actions", "delete") %>
</button>
<% end %>
</div>
<hr class="hr" />
<h2 class="pt-2 self-center text-lg">
<%= gettext("steps:") %>
</h2>
<%= if @steps |> Enum.empty?() do %>
<h3 class="self-center text-md text-primary-600">
<%= gettext("no steps") %>
</h3>
<% else %>
<%= for %{id: step_id, position: position, title: title} = step <- @steps do %>
<div class="flex justify-between items-center space-x-4">
<h3 class="text-md">
<%= gettext("%{position}. %{title}", position: position + 1, title: title) %>
</h3>
<%= if is_owner?(@pipeline, @current_user) do %>
<div class="flex justify-between items-center space-x-4">
<%= if position <= 0 do %>
<i class="fas text-xl fa-chevron-up cursor-not-allowed opacity-25"></i>
<% else %>
<button
type="button"
class="cursor-pointer flex justify-center items-center"
phx-click="reorder_step"
phx-value-direction="up"
phx-value-step-id={step_id}
data-qa={"move-step-up-#{step_id}"}
>
<i class="fas text-xl fa-chevron-up"></i>
</button>
<% end %>
<%= if position >= length(@steps) - 1 do %>
<i class="fas text-xl fa-chevron-down cursor-not-allowed opacity-25"></i>
<% else %>
<button
type="button"
class="cursor-pointer flex justify-center items-center"
phx-click="reorder_step"
phx-value-direction="down"
phx-value-step-id={step_id}
data-qa={"move-step-down-#{step_id}"}
>
<i class="fas text-xl fa-chevron-down"></i>
</button>
<% end %>
<.link
class="self-end btn btn-primary"
patch={Routes.pipeline_show_path(@socket, :edit_step, @pipeline.slug, step_id)}
data-qa={"edit-step-#{step_id}"}
>
<%= dgettext("actions", "edit") %>
</.link>
<button
type="button"
class="btn btn-primary"
phx-click="delete_step"
phx-value-step-id={step_id}
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-step-#{step_id}"}
>
<%= dgettext("actions", "delete") %>
</button>
</div>
<% end %>
</div>
<.step_content step={step} />
<% end %>
<% end %>
<%= if is_owner?(@pipeline, @current_user) do %>
<.link
class="self-end btn btn-primary"
patch={Routes.pipeline_show_path(@socket, :add_step, @pipeline.slug)}
data-qa={"add-step-#{@pipeline.id}"}
>
<%= dgettext("actions", "add step") %>
</.link>
<% end %>
</div>
<%= case @live_action do %>
<% :edit -> %>
<.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}>
<.live_component
module={MemexWeb.PipelineLive.FormComponent}
id={@pipeline.id}
current_user={@current_user}
title={@page_title}
action={@live_action}
pipeline={@pipeline}
return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}
/>
</.modal>
<% :add_step -> %>
<.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}>
<.live_component
module={MemexWeb.StepLive.FormComponent}
id={@pipeline.id || :new}
current_user={@current_user}
title={@page_title}
action={@live_action}
pipeline={@pipeline}
step={@step}
return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}
/>
</.modal>
<% :edit_step -> %>
<.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}>
<.live_component
module={MemexWeb.StepLive.FormComponent}
id={@pipeline.id || :new}
current_user={@current_user}
title={@page_title}
action={@live_action}
pipeline={@pipeline}
step={@step}
return_to={Routes.pipeline_show_path(@socket, :show, @pipeline.slug)}
/>
</.modal>
<% _ -> %>
<% end %> <% end %>
<ul>
<li>
<strong>Title:</strong>
<%= @pipeline.title %>
</li>
<li>
<strong>Description:</strong>
<%= @pipeline.description %>
</li>
<li>
<strong>Visibility:</strong>
<%= @pipeline.visibility %>
</li>
</ul>
<span>
<.link patch={Routes.pipeline_show_path(@socket, :edit, @pipeline)} class="button">
<%= dgettext("actions", "edit") %>
</.link>
</span>
|
<span>
<.link patch={Routes.pipeline_index_path(@socket, :index)}>
<%= dgettext("actions", "Back") %>
</.link>
</span>

View File

@ -0,0 +1,74 @@
defmodule MemexWeb.StepLive.FormComponent do
use MemexWeb, :live_component
alias Memex.Pipelines.Steps
@impl true
def update(%{step: step, current_user: current_user, pipeline: _pipeline} = assigns, socket) do
changeset = Steps.change_step(step, current_user)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
@impl true
def handle_event(
"validate",
%{"step" => step_params},
%{assigns: %{step: step, current_user: current_user}} = socket
) do
changeset =
step
|> Steps.change_step(step_params, current_user)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"step" => step_params}, %{assigns: %{action: action}} = socket) do
save_step(socket, action, step_params)
end
defp save_step(
%{assigns: %{step: step, return_to: return_to, current_user: current_user}} = socket,
:edit_step,
step_params
) do
case Steps.update_step(step, step_params, current_user) do
{:ok, %{title: title}} ->
{:noreply,
socket
|> put_flash(:info, gettext("%{title} saved", title: title))
|> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
defp save_step(
%{
assigns: %{
step: %{position: position},
return_to: return_to,
current_user: current_user,
pipeline: pipeline
}
} = socket,
:add_step,
step_params
) do
case Steps.create_step(step_params, position, pipeline, current_user) do
{:ok, %{title: title}} ->
{:noreply,
socket
|> put_flash(:info, gettext("%{title} created", title: title))
|> push_navigate(to: return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View File

@ -0,0 +1,34 @@
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
<.form
:let={f}
for={@changeset}
id="step-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
phx-debounce="300"
class="flex flex-col justify-start items-stretch space-y-4"
>
<%= text_input(f, :title,
class: "input input-primary",
placeholder: gettext("title")
) %>
<%= error_tag(f, :title) %>
<%= textarea(f, :content,
id: "step-form-content",
class: "input input-primary h-64 min-h-64",
phx_hook: "MaintainAttrs",
phx_update: "ignore",
placeholder: gettext("use [[context-slug]] to link to a context")
) %>
<%= error_tag(f, :content) %>
<div class="flex justify-center items-stretch space-x-4">
<%= submit(dgettext("actions", "save"),
phx_disable_with: gettext("saving..."),
class: "mx-auto btn btn-primary"
) %>
</div>
</.form>
</div>

View File

@ -0,0 +1,3 @@
defmodule MemexWeb.NotFoundError do
defexception [:message, plug_status: 404]
end

View File

@ -36,6 +36,7 @@ defmodule MemexWeb.Router do
pipe_through :browser pipe_through :browser
live "/", HomeLive live "/", HomeLive
live "/faq", FaqLive
end end
## Authentication routes ## Authentication routes
@ -58,16 +59,18 @@ defmodule MemexWeb.Router do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
live "/notes/new", NoteLive.Index, :new live "/notes/new", NoteLive.Index, :new
live "/notes/:id/edit", NoteLive.Index, :edit live "/notes/:slug/edit", NoteLive.Index, :edit
live "/note/:id/edit", NoteLive.Show, :edit live "/note/:slug/edit", NoteLive.Show, :edit
live "/contexts/new", ContextLive.Index, :new live "/contexts/new", ContextLive.Index, :new
live "/contexts/:id/edit", ContextLive.Index, :edit live "/contexts/:slug/edit", ContextLive.Index, :edit
live "/contexts/:id/show/edit", ContextLive.Show, :edit live "/context/:slug/edit", ContextLive.Show, :edit
live "/pipelines/new", PipelineLive.Index, :new live "/pipelines/new", PipelineLive.Index, :new
live "/pipelines/:id/edit", PipelineLive.Index, :edit live "/pipelines/:slug/edit", PipelineLive.Index, :edit
live "/pipelines/:id/show/edit", PipelineLive.Show, :edit live "/pipeline/:slug/edit", PipelineLive.Show, :edit
live "/pipeline/:slug/add_step", PipelineLive.Show, :add_step
live "/pipeline/:slug/:step_id", PipelineLive.Show, :edit_step
get "/users/settings", UserSettingsController, :edit get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update put "/users/settings", UserSettingsController, :update
@ -80,13 +83,15 @@ defmodule MemexWeb.Router do
live "/notes", NoteLive.Index, :index live "/notes", NoteLive.Index, :index
live "/notes/:search", NoteLive.Index, :search live "/notes/:search", NoteLive.Index, :search
live "/note/:id", NoteLive.Show, :show live "/note/:slug", NoteLive.Show, :show
live "/contexts", ContextLive.Index, :index live "/contexts", ContextLive.Index, :index
live "/contexts/:id", ContextLive.Show, :show live "/contexts/:search", ContextLive.Index, :search
live "/context/:slug", ContextLive.Show, :show
live "/pipelines", PipelineLive.Index, :index live "/pipelines", PipelineLive.Index, :index
live "/pipelines/:id", PipelineLive.Show, :show live "/pipelines/:search", PipelineLive.Index, :search
live "/pipeline/:slug", PipelineLive.Show, :show
end end
end end

View File

@ -11,7 +11,7 @@
<script defer type="text/javascript" src="/js/app.js"> <script defer type="text/javascript" src="/js/app.js">
</script> </script>
</head> </head>
<body class="pb-8 m-0 p-0 w-full h-full"> <body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
<header> <header>
<.topbar current_user={assigns[:current_user]}></.topbar> <.topbar current_user={assigns[:current_user]}></.topbar>
</header> </header>
@ -25,7 +25,7 @@
<hr class="w-full hr" /> <hr class="w-full hr" />
<a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg"> <a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg">
<%= dgettext("errors", "Go back home") %> <%= dgettext("errors", "go back home") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<.live_title suffix={" | #{gettext("memex")}"}> <.live_title suffix={" | #{gettext("memEx")}"}>
<%= assigns[:page_title] || gettext("memex") %> <%= assigns[:page_title] || gettext("memEx") %>
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} /> <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<script <script

View File

@ -6,9 +6,9 @@ defmodule MemexWeb.ErrorView do
def template_not_found(error_path, _assigns) do def template_not_found(error_path, _assigns) do
error_string = error_string =
case error_path do case error_path do
"404.html" -> dgettext("errors", "Not found") "404.html" -> dgettext("errors", "not found")
"401.html" -> dgettext("errors", "Unauthorized") "401.html" -> dgettext("errors", "unauthorized")
_ -> dgettext("errors", "Internal Server Error") _ -> dgettext("errors", "internal server error")
end end
render("error.html", %{error_string: error_string}) render("error.html", %{error_string: error_string})

View File

@ -15,9 +15,9 @@ defmodule Memex.MixProject do
consolidate_protocols: Mix.env() not in [:dev, :test], consolidate_protocols: Mix.env() not in [:dev, :test],
preferred_cli_env: [test: :test, "test.all": :test], preferred_cli_env: [test: :test, "test.all": :test],
# ExDoc # ExDoc
name: "memex", name: "memEx",
source_url: "https://gitea.bubbletea.dev/shibao/memex", source_url: "https://gitea.bubbletea.dev/shibao/memEx",
homepage_url: "https://gitea.bubbletea.dev/shibao/memex", homepage_url: "https://gitea.bubbletea.dev/shibao/memEx",
docs: [ docs: [
# The main page in the docs # The main page in the docs
main: "README.md", main: "README.md",

View File

@ -10,95 +10,148 @@
msgid "" msgid ""
msgstr "" msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:113
msgid "Change Language"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:15
#: lib/memex_web/templates/user_settings/edit.html.heex:44
msgid "Change email"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:131
msgid "Change language"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:58
#: lib/memex_web/templates/user_settings/edit.html.heex:99
msgid "Change password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:32
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:16
msgid "Create Invite"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:139
msgid "Delete User"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:51 #: lib/memex_web/templates/user_registration/new.html.heex:51
#: lib/memex_web/templates/user_reset_password/new.html.heex:3 #: lib/memex_web/templates/user_reset_password/new.html.heex:3
#: lib/memex_web/templates/user_session/new.html.heex:44 #: lib/memex_web/templates/user_session/new.html.heex:44
#, elixir-autogen, elixir-format
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_confirmation/new.html.heex:3
#: lib/memex_web/templates/user_confirmation/new.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:11 msgid "Resend confirmation instructions"
msgid "Invite someone new!"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_reset_password/edit.html.heex:3
#: lib/memex_web/templates/user_reset_password/edit.html.heex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:119 msgid "Reset password"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:28
#, elixir-autogen, elixir-format
msgid "Save"
msgstr ""
#: lib/memex_web/templates/user_reset_password/new.html.heex:15
#, elixir-autogen, elixir-format
msgid "Send instructions to reset password"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:15
#: lib/memex_web/templates/user_settings/edit.html.heex:44
#, elixir-autogen, elixir-format
msgid "change email"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:113
#: lib/memex_web/templates/user_settings/edit.html.heex:131
#, elixir-autogen, elixir-format
msgid "change language"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:58
#: lib/memex_web/templates/user_settings/edit.html.heex:99
#, elixir-autogen, elixir-format
msgid "change password"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:16
#, elixir-autogen, elixir-format
msgid "create invite"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:49
#: lib/memex_web/live/context_live/show.html.heex:34
#: lib/memex_web/live/note_live/index.html.heex:49
#: lib/memex_web/live/note_live/show.html.heex:38
#: lib/memex_web/live/pipeline_live/index.html.heex:49
#: lib/memex_web/live/pipeline_live/show.html.heex:43
#: lib/memex_web/live/pipeline_live/show.html.heex:113
#, elixir-autogen, elixir-format
msgid "delete"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:145
#, elixir-autogen, elixir-format
msgid "delete user"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:38
#: lib/memex_web/live/context_live/show.html.heex:23
#: lib/memex_web/live/note_live/index.html.heex:38
#: lib/memex_web/live/note_live/show.html.heex:27
#: lib/memex_web/live/pipeline_live/index.html.heex:38
#: lib/memex_web/live/pipeline_live/show.html.heex:32
#: lib/memex_web/live/pipeline_live/show.html.heex:102
#, elixir-autogen, elixir-format
msgid "edit"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:12
#, elixir-autogen, elixir-format
msgid "invite someone new!"
msgstr ""
#: lib/memex_web/components/topbar.ex:125
#: lib/memex_web/templates/user_confirmation/new.html.heex:29 #: lib/memex_web/templates/user_confirmation/new.html.heex:29
#: lib/memex_web/templates/user_registration/new.html.heex:47 #: lib/memex_web/templates/user_registration/new.html.heex:48
#: lib/memex_web/templates/user_reset_password/edit.html.heex:47 #: lib/memex_web/templates/user_reset_password/edit.html.heex:47
#: lib/memex_web/templates/user_reset_password/new.html.heex:29 #: lib/memex_web/templates/user_reset_password/new.html.heex:29
#: lib/memex_web/templates/user_session/new.html.heex:3 #: lib/memex_web/templates/user_session/new.html.heex:3
#: lib/memex_web/templates/user_session/new.html.heex:32 #: lib/memex_web/templates/user_session/new.html.heex:32
msgid "Log in" #, elixir-autogen, elixir-format
msgid "log in"
msgstr "" msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:58
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:111 msgid "new context"
#: lib/memex_web/templates/user_confirmation/new.html.heex:24 msgstr ""
#: lib/memex_web/live/note_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new note"
msgstr ""
#: lib/memex_web/live/pipeline_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new pipeline"
msgstr ""
#: lib/memex_web/components/topbar.ex:115
#: lib/memex_web/templates/user_confirmation/new.html.heex:25
#: lib/memex_web/templates/user_registration/new.html.heex:3 #: lib/memex_web/templates/user_registration/new.html.heex:3
#: lib/memex_web/templates/user_registration/new.html.heex:41 #: lib/memex_web/templates/user_registration/new.html.heex:41
#: lib/memex_web/templates/user_reset_password/edit.html.heex:42 #: lib/memex_web/templates/user_reset_password/edit.html.heex:43
#: lib/memex_web/templates/user_reset_password/new.html.heex:24 #: lib/memex_web/templates/user_reset_password/new.html.heex:25
#: lib/memex_web/templates/user_session/new.html.heex:39 #: lib/memex_web/templates/user_session/new.html.heex:40
msgid "Register" #, elixir-autogen, elixir-format
msgid "register"
msgstr "" msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:42
#: lib/memex_web/live/note_live/form_component.html.heex:42
#: lib/memex_web/live/pipeline_live/form_component.html.heex:42
#: lib/memex_web/live/step_live/form_component.html.heex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_confirmation/new.html.heex:3 msgid "save"
#: lib/memex_web/templates/user_confirmation/new.html.heex:15
msgid "Resend confirmation instructions"
msgstr "" msgstr ""
#: lib/memex_web/live/context_live/show.html.heex:16
#: lib/memex_web/live/note_live/show.html.heex:23
#: lib/memex_web/live/pipeline_live/show.html.heex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_reset_password/edit.html.heex:3 msgid "back"
#: lib/memex_web/templates/user_reset_password/edit.html.heex:33
msgid "Reset password"
msgstr "" msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:129
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:28 msgid "add step"
msgid "Save"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_reset_password/new.html.heex:15
msgid "Send instructions to reset password"
msgstr "" msgstr ""

View File

@ -10,297 +10,627 @@
msgid "" msgid ""
msgstr "" msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:90
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:73
msgid "Accessible from any internet-capable device"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:89
msgid "Admins" msgid "Admins"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:53
msgid "Built with sharing and collaboration in mind"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:78
msgid "Confirm new password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:8 #: lib/memex_web/controllers/user_confirmation_controller.ex:8
#, elixir-autogen, elixir-format
msgid "Confirm your account" msgid "Confirm your account"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:63
msgid "Contexts"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:22
msgid "Contexts:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:70
msgid "Convenient:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:32
#: lib/memex_web/templates/user_settings/edit.html.heex:87
msgid "Current password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:58
msgid "Disable"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:15
msgid "Document notes about individual items or concepts"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:35
msgid "Document your processes, attaching contexts to each step"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:33
msgid "Edit Invite"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:62
msgid "Enable"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:36 #: lib/memex_web/templates/user_registration/new.html.heex:36
#: lib/memex_web/templates/user_settings/edit.html.heex:126 #, elixir-autogen, elixir-format
msgid "English" msgid "English"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:44
msgid "Features"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:9 #: lib/memex_web/controllers/user_reset_password_controller.ex:9
#, elixir-autogen, elixir-format
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.ex:12
msgid "Home"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/invite_card.ex:25
msgid "Invite Disabled"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:78
#: lib/memex_web/live/invite_live/index.ex:41
#: lib/memex_web/live/invite_live/index.html.heex:3
msgid "Invites"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_session/new.html.heex:27 #: lib/memex_web/templates/user_session/new.html.heex:27
#, elixir-autogen, elixir-format
msgid "Keep me logged in for 60 days" msgid "Keep me logged in for 60 days"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:32 #: lib/memex_web/templates/user_registration/new.html.heex:32
#, elixir-autogen, elixir-format
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/live.html.heex:37 #: lib/memex_web/templates/layout/live.html.heex:37
#, elixir-autogen, elixir-format
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_session_controller.ex:8
msgid "Log in"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:50
msgid "Multi-user:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:20 #: lib/memex_web/live/invite_live/form_component.html.heex:20
#, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:37
msgid "New Invite"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:71
msgid "New password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:8
msgid "No invites 😔"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:70
msgid "Notes"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:12
msgid "Notes:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:56
msgid "Pipelines"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:32
msgid "Pipelines:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:63
msgid "Privacy controls on a per-note, context or pipeline basis"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:60
msgid "Privacy:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:25
msgid "Provide context around a single topic and hotlink to your notes"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/live.html.heex:50 #: lib/memex_web/templates/layout/live.html.heex:50
#, elixir-autogen, elixir-format
msgid "Reconnecting..." msgid "Reconnecting..."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:35
msgid "Register"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:36 #: lib/memex_web/controllers/user_reset_password_controller.ex:36
#, elixir-autogen, elixir-format
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:78
msgid "Set Unlimited"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:10 #: lib/memex_web/controllers/user_settings_controller.ex:10
#: lib/memex_web/templates/user_settings/edit.html.heex:3 #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: lib/memex_web/components/user_card.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/components/user_card.ex:30
msgid "User registered on" msgid "User registered on"
msgstr "" msgstr ""
#: lib/memex_web/components/invite_card.ex:19
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:118
msgid "Users"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/invite_card.ex:20
msgid "Uses Left:" msgid "Uses Left:"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:24 #: lib/memex_web/live/invite_live/form_component.html.heex:24
#, elixir-autogen, elixir-format
msgid "Uses left" msgid "Uses left"
msgstr "" msgstr ""
#: lib/memex_web/live/context_live/show.html.heex:11
#: lib/memex_web/live/note_live/show.html.heex:18
#: lib/memex_web/live/pipeline_live/show.html.heex:20
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Visibility: %{visibility}"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:76
#, elixir-autogen, elixir-format
msgid "accessible from any internet-capable device"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:90
#, elixir-autogen, elixir-format
msgid "admins:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:58
#, elixir-autogen, elixir-format
msgid "built with sharing and collaboration in mind"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:78
#, elixir-autogen, elixir-format
msgid "confirm new password"
msgstr ""
#: lib/memex_web/components/contexts_table_component.ex:48
#: lib/memex_web/components/notes_table_component.ex:48
#: lib/memex_web/live/note_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "content"
msgstr ""
#: lib/memex_web/components/topbar.ex:52
#: lib/memex_web/live/context_live/index.ex:35
#: lib/memex_web/live/context_live/index.ex:43
#: lib/memex_web/live/context_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "contexts"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:20
#, elixir-autogen, elixir-format
msgid "contexts:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:73
#, elixir-autogen, elixir-format
msgid "convenient:"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:32
#: lib/memex_web/templates/user_settings/edit.html.heex:87
#, elixir-autogen, elixir-format
msgid "current password"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:59
#, elixir-autogen, elixir-format
msgid "disable"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:14
#, elixir-autogen, elixir-format
msgid "document notes about individual items or concepts"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:32
#, elixir-autogen, elixir-format
msgid "document your processes, attaching contexts to each step"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:33
#, elixir-autogen, elixir-format
msgid "edit invite"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:28
#, elixir-autogen, elixir-format
msgid "email"
msgstr ""
#: lib/memex_web/components/user_card.ex:23
#, elixir-autogen, elixir-format
msgid "email unconfirmed"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:63
#, elixir-autogen, elixir-format
msgid "enable"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:126
#, elixir-autogen, elixir-format
msgid "english"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:50
#, elixir-autogen, elixir-format
msgid "features"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:138
#, elixir-autogen, elixir-format
msgid "get involved!"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:159
#, elixir-autogen, elixir-format
msgid "help translate"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:85
#, elixir-autogen, elixir-format
msgid "instance information"
msgstr ""
#: lib/memex_web/components/invite_card.ex:24
#, elixir-autogen, elixir-format
msgid "invite disabled"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:115
#, elixir-autogen, elixir-format
msgid "invite only"
msgstr ""
#: lib/memex_web/components/topbar.ex:74
#: lib/memex_web/live/invite_live/index.ex:41
#: lib/memex_web/live/invite_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "invites"
msgstr ""
#: lib/memex_web/controllers/user_session_controller.ex:8
#, elixir-autogen, elixir-format
msgid "log in"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:55
#, elixir-autogen, elixir-format
msgid "multi-user:"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:37
#, elixir-autogen, elixir-format
msgid "new invite"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:71
#, elixir-autogen, elixir-format
msgid "new password"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:8
#, elixir-autogen, elixir-format
msgid "no invites 😔"
msgstr ""
#: lib/memex_web/live/note_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "no notes found"
msgstr ""
#: lib/memex_web/components/topbar.ex:43
#: lib/memex_web/live/note_live/index.ex:35
#: lib/memex_web/live/note_live/index.ex:43
#: lib/memex_web/live/note_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "notes"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:11
#, elixir-autogen, elixir-format
msgid "notes:"
msgstr ""
#: lib/memex_web/components/topbar.ex:61
#: lib/memex_web/live/pipeline_live/index.ex:35
#: lib/memex_web/live/pipeline_live/index.ex:43
#: lib/memex_web/live/pipeline_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "pipelines"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:29
#, elixir-autogen, elixir-format
msgid "pipelines:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:67
#, elixir-autogen, elixir-format
msgid "privacy controls on a per-note, context or pipeline basis"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:64
#, elixir-autogen, elixir-format
msgid "privacy:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:23
#, elixir-autogen, elixir-format
msgid "provide context around a single topic and hotlink to your notes"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:114
#, elixir-autogen, elixir-format
msgid "public signups"
msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:34
#, elixir-autogen, elixir-format
msgid "register"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:110
#, elixir-autogen, elixir-format
msgid "registration:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:170
#, elixir-autogen, elixir-format
msgid "report bugs or request features"
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:43
#: lib/memex_web/live/note_live/form_component.html.heex:43
#: lib/memex_web/live/pipeline_live/form_component.html.heex:43
#: lib/memex_web/live/step_live/form_component.html.heex:29
#, elixir-autogen, elixir-format
msgid "saving..."
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:39
#: lib/memex_web/live/note_live/form_component.html.heex:39
#: lib/memex_web/live/pipeline_live/form_component.html.heex:39
#, elixir-autogen, elixir-format
msgid "select privacy"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:79
#, elixir-autogen, elixir-format
msgid "set unlimited"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:3
#, elixir-autogen, elixir-format
msgid "settings"
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:30
#: lib/memex_web/live/note_live/form_component.html.heex:30
#: lib/memex_web/live/pipeline_live/form_component.html.heex:30
#, elixir-autogen, elixir-format
msgid "tag1,tag2"
msgstr ""
#: lib/memex_web/components/contexts_table_component.ex:49
#: lib/memex_web/components/notes_table_component.ex:49
#: lib/memex_web/components/pipelines_table_component.ex:49
#, elixir-autogen, elixir-format
msgid "tags"
msgstr ""
#: lib/memex_web/components/invite_card.ex:20
#, elixir-autogen, elixir-format
msgid "unlimited"
msgstr ""
#: lib/memex_web/components/user_card.ex:25
#, elixir-autogen, elixir-format
msgid "user was confirmed at %{relative_datetime}"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:120
#, elixir-autogen, elixir-format
msgid "users"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:121
#, elixir-autogen, elixir-format
msgid "version:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:148
#, elixir-autogen, elixir-format
msgid "view the source code"
msgstr ""
#: lib/memex_web/components/contexts_table_component.ex:50
#: lib/memex_web/components/notes_table_component.ex:50
#: lib/memex_web/components/pipelines_table_component.ex:50
#, elixir-autogen, elixir-format
msgid "visibility"
msgstr ""
#: lib/memex_web/live/note_live/index.ex:29
#, elixir-autogen, elixir-format
msgid "new note"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:17
#: lib/memex_web/live/note_live/index.html.heex:17
#: lib/memex_web/live/pipeline_live/index.html.heex:17
#, elixir-autogen, elixir-format
msgid "search"
msgstr ""
#: lib/memex_web/live/context_live/index.ex:29
#, elixir-autogen, elixir-format
msgid "new context"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "no contexts found"
msgstr ""
#: lib/memex_web/components/pipelines_table_component.ex:48
#: lib/memex_web/live/pipeline_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "description"
msgstr ""
#: lib/memex_web/live/pipeline_live/index.ex:29
#, elixir-autogen, elixir-format
msgid "new pipeline"
msgstr ""
#: lib/memex_web/live/pipeline_live/index.html.heex:23
#, elixir-autogen, elixir-format
msgid "no pipelines found"
msgstr ""
#: lib/memex_web/live/context_live/form_component.ex:61
#: lib/memex_web/live/note_live/form_component.ex:60
#: lib/memex_web/live/pipeline_live/form_component.ex:65
#, elixir-autogen, elixir-format
msgid "%{slug} created"
msgstr ""
#: lib/memex_web/live/context_live/index.ex:57
#: lib/memex_web/live/context_live/show.ex:41
#: lib/memex_web/live/note_live/index.ex:57
#: lib/memex_web/live/note_live/show.ex:41
#: lib/memex_web/live/pipeline_live/index.ex:57
#: lib/memex_web/live/pipeline_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "%{slug} deleted"
msgstr ""
#: lib/memex_web/live/context_live/form_component.ex:44
#: lib/memex_web/live/note_live/form_component.ex:43
#: lib/memex_web/live/pipeline_live/form_component.ex:48
#, elixir-autogen, elixir-format
msgid "%{slug} saved"
msgstr ""
#: lib/memex_web/live/context_live/index.ex:23
#: lib/memex_web/live/context_live/show.ex:48
#: lib/memex_web/live/note_live/index.ex:23
#: lib/memex_web/live/note_live/show.ex:48
#: lib/memex_web/live/pipeline_live/index.ex:23
#: lib/memex_web/live/pipeline_live/show.ex:125
#, elixir-autogen, elixir-format
msgid "edit %{slug}"
msgstr ""
#: lib/memex_web/components/contexts_table_component.ex:47
#: lib/memex_web/components/notes_table_component.ex:47
#: lib/memex_web/components/pipelines_table_component.ex:47
#: lib/memex_web/live/context_live/form_component.html.heex:14
#: lib/memex_web/live/note_live/form_component.html.heex:14
#: lib/memex_web/live/pipeline_live/form_component.html.heex:14
#, elixir-autogen, elixir-format
msgid "slug"
msgstr ""
#: lib/memex_web/live/context_live/show.ex:19
#: lib/memex_web/live/note_live/show.ex:19
#: lib/memex_web/live/pipeline_live/show.ex:20
#, elixir-autogen, elixir-format
msgid "%{slug} could not be found"
msgstr ""
#: lib/memex_web/live/home_live.ex:13
#, elixir-autogen, elixir-format
msgid "home"
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "use [[note-slug]] to link to a note"
msgstr ""
#: lib/memex_web/live/faq_live.ex:10
#: lib/memex_web/live/faq_live.html.heex:3
#, elixir-autogen, elixir-format
msgid "faq"
msgstr ""
#: lib/memex_web/components/topbar.ex:23
#: lib/memex_web/live/home_live.html.heex:3 #: lib/memex_web/live/home_live.html.heex:3
#: lib/memex_web/templates/layout/root.html.heex:8
#: lib/memex_web/templates/layout/root.html.heex:9
#, elixir-autogen, elixir-format
msgid "memEx"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:41
#, elixir-autogen, elixir-format
msgid "read more on how to use %{name}"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:11
#, elixir-autogen, elixir-format
msgid "what is this?"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:62
#, elixir-autogen, elixir-format
msgid "%{position}. %{title}"
msgstr ""
#: lib/memex_web/live/step_live/form_component.ex:67
#, elixir-autogen, elixir-format
msgid "%{title} created"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.ex:96
#, elixir-autogen, elixir-format
msgid "%{title} deleted"
msgstr ""
#: lib/memex_web/live/step_live/form_component.ex:43
#, elixir-autogen, elixir-format
msgid "%{title} saved"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.ex:127
#, elixir-autogen, elixir-format
msgid "add step to %{slug}"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:56
#, elixir-autogen, elixir-format
msgid "no steps"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:51
#, elixir-autogen, elixir-format
msgid "steps:"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:14
#, elixir-autogen, elixir-format
msgid "title"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "use [[context-slug]] to link to a context"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:72
#, elixir-autogen, elixir-format
msgid "finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:102
#, elixir-autogen, elixir-format
msgid "for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:118
#, elixir-autogen, elixir-format
msgid "for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:62
#, elixir-autogen, elixir-format
msgid "i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:67
#, elixir-autogen, elixir-format
msgid "i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:99
#, elixir-autogen, elixir-format
msgid "in my opinion, contexts should be like single-topic blog posts."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:83
#, elixir-autogen, elixir-format
msgid "in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:113
#, elixir-autogen, elixir-format
msgid "in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:31
#, elixir-autogen, elixir-format
msgid "memex" msgid "memex"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:87 msgid "org-mode"
msgid "Admins:"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:20
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/note_live/form_component.html.heex:20 msgid "some things that this memex is very loosely inspired by:"
msgid "Content"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:134 msgid "spoons? probably not. a particular brand of spoons that you really like? why not :)"
msgid "Get involved!"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:14
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:151 msgid "this is a memex, used to document not just your notes, but also your perspectives and processes."
msgid "Help translate"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:82 msgid "what should my contexts be like?"
msgid "Instance Information"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:80
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:113 msgid "what should my notes be like?"
msgid "Invite Only"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:110
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:112 msgid "what should my pipelines be like?"
msgid "Public Signups"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:160 msgid "why split up into notes, contexts and pipelines?"
msgid "Report bugs or request features"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:41
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/note_live/form_component.html.heex:35 msgid "zettelkasten"
msgid "Save"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/note_live/form_component.html.heex:36
msgid "Saving..."
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/note_live/form_component.html.heex:13
msgid "Title"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:142
msgid "View the source code"
msgstr "" msgstr ""

View File

@ -10,83 +10,83 @@
msgid "" msgid ""
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex/accounts/email.ex:30 #: lib/memex/accounts/email.ex:30
#, elixir-autogen, elixir-format
msgid "Confirm your Memex account" msgid "Confirm your Memex account"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/confirm_email.html.heex:3 #: lib/memex_web/templates/email/confirm_email.html.heex:3
#: lib/memex_web/templates/email/confirm_email.txt.eex:2 #: lib/memex_web/templates/email/confirm_email.txt.eex:2
#: lib/memex_web/templates/email/reset_password.html.heex:3 #: lib/memex_web/templates/email/reset_password.html.heex:3
#: lib/memex_web/templates/email/reset_password.txt.eex:2 #: lib/memex_web/templates/email/reset_password.txt.eex:2
#: lib/memex_web/templates/email/update_email.html.heex:3 #: lib/memex_web/templates/email/update_email.html.heex:3
#: lib/memex_web/templates/email/update_email.txt.eex:2 #: lib/memex_web/templates/email/update_email.txt.eex:2
#, elixir-autogen, elixir-format
msgid "Hi %{email}," msgid "Hi %{email},"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/confirm_email.txt.eex:10 #: lib/memex_web/templates/email/confirm_email.txt.eex:10
#, elixir-autogen, elixir-format
msgid "If you didn't create an account at %{url}, please ignore this." msgid "If you didn't create an account at %{url}, please ignore this."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/confirm_email.html.heex:22 #: lib/memex_web/templates/email/confirm_email.html.heex:22
#, elixir-autogen, elixir-format
msgid "If you didn't create an account at Memex, please ignore this." msgid "If you didn't create an account at Memex, please ignore this."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/reset_password.txt.eex:8 #: lib/memex_web/templates/email/reset_password.txt.eex:8
#: lib/memex_web/templates/email/update_email.txt.eex:8 #: lib/memex_web/templates/email/update_email.txt.eex:8
#, elixir-autogen, elixir-format
msgid "If you didn't request this change from %{url}, please ignore this." msgid "If you didn't request this change from %{url}, please ignore this."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/reset_password.html.heex:16 #: lib/memex_web/templates/email/reset_password.html.heex:16
#: lib/memex_web/templates/email/update_email.html.heex:16 #: lib/memex_web/templates/email/update_email.html.heex:16
#, elixir-autogen, elixir-format
msgid "If you didn't request this change from Memex, please ignore this." msgid "If you didn't request this change from Memex, please ignore this."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex/accounts/email.ex:37 #: lib/memex/accounts/email.ex:37
#, elixir-autogen, elixir-format
msgid "Reset your Memex password" msgid "Reset your Memex password"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/email.txt.eex:9 #: lib/memex_web/templates/layout/email.txt.eex:9
#, elixir-autogen, elixir-format
msgid "This email was sent from Memex at %{url}, the self-hosted firearm tracker website." msgid "This email was sent from Memex at %{url}, the self-hosted firearm tracker website."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/email.html.heex:13 #: lib/memex_web/templates/layout/email.html.heex:13
#, elixir-autogen, elixir-format
msgid "This email was sent from Memex, the self-hosted firearm tracker website." msgid "This email was sent from Memex, the self-hosted firearm tracker website."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex/accounts/email.ex:44 #: lib/memex/accounts/email.ex:44
#, elixir-autogen, elixir-format
msgid "Update your Memex email" msgid "Update your Memex email"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/confirm_email.html.heex:9 #: lib/memex_web/templates/email/confirm_email.html.heex:9
#: lib/memex_web/templates/email/confirm_email.txt.eex:4 #: lib/memex_web/templates/email/confirm_email.txt.eex:4
#, elixir-autogen, elixir-format
msgid "Welcome to Memex" msgid "Welcome to Memex"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/update_email.html.heex:8 #: lib/memex_web/templates/email/update_email.html.heex:8
#: lib/memex_web/templates/email/update_email.txt.eex:4 #: lib/memex_web/templates/email/update_email.txt.eex:4
#, elixir-autogen, elixir-format
msgid "You can change your email by visiting the URL below:" msgid "You can change your email by visiting the URL below:"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/confirm_email.html.heex:14 #: lib/memex_web/templates/email/confirm_email.html.heex:14
#: lib/memex_web/templates/email/confirm_email.txt.eex:6 #: lib/memex_web/templates/email/confirm_email.txt.eex:6
#, elixir-autogen, elixir-format
msgid "You can confirm your account by visiting the URL below:" msgid "You can confirm your account by visiting the URL below:"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/email/reset_password.html.heex:8 #: lib/memex_web/templates/email/reset_password.html.heex:8
#: lib/memex_web/templates/email/reset_password.txt.eex:4 #: lib/memex_web/templates/email/reset_password.txt.eex:4
#, elixir-autogen, elixir-format
msgid "You can reset your password by visiting the URL below:" msgid "You can reset your password by visiting the URL below:"
msgstr "" msgstr ""

View File

@ -10,109 +10,123 @@
msgid "" msgid ""
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:84 #: lib/memex_web/controllers/user_settings_controller.ex:84
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired." msgid "Email change link is invalid or it has expired."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/error/error.html.heex:8 #: lib/memex_web/templates/error/error.html.heex:8
#, elixir-autogen, elixir-format
msgid "Error" msgid "Error"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/error/error.html.heex:28
msgid "Go back home"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/views/error_view.ex:11
msgid "Internal Server Error"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_session_controller.ex:17 #: lib/memex_web/controllers/user_session_controller.ex:17
#, elixir-autogen, elixir-format
msgid "Invalid email or password" msgid "Invalid email or password"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/views/error_view.ex:9
msgid "Not found"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:15 #: lib/memex_web/templates/user_registration/new.html.heex:15
#: lib/memex_web/templates/user_reset_password/edit.html.heex:15 #: lib/memex_web/templates/user_reset_password/edit.html.heex:15
#: lib/memex_web/templates/user_settings/edit.html.heex:21
#: lib/memex_web/templates/user_settings/edit.html.heex:64 #: lib/memex_web/templates/user_settings/edit.html.heex:64
#: lib/memex_web/templates/user_settings/edit.html.heex:119 #, elixir-autogen, elixir-format
msgid "Oops, something went wrong! Please check the errors below." msgid "Oops, something went wrong! Please check the errors below."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:63 #: lib/memex_web/controllers/user_reset_password_controller.ex:63
#, elixir-autogen, elixir-format
msgid "Reset password link is invalid or it has expired." msgid "Reset password link is invalid or it has expired."
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:24
#: lib/memex_web/controllers/user_registration_controller.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:25
#: lib/memex_web/controllers/user_registration_controller.ex:56
msgid "Sorry, public registration is disabled" msgid "Sorry, public registration is disabled"
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:14
#: lib/memex_web/controllers/user_registration_controller.ex:45
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:15
#: lib/memex_web/controllers/user_registration_controller.ex:46
msgid "Sorry, this invite was not found or expired" msgid "Sorry, this invite was not found or expired"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:99 #: lib/memex_web/controllers/user_settings_controller.ex:99
#, elixir-autogen, elixir-format
msgid "Unable to delete user" msgid "Unable to delete user"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/views/error_view.ex:10
msgid "Unauthorized"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:54 #: lib/memex_web/controllers/user_confirmation_controller.ex:54
#, elixir-autogen, elixir-format
msgid "User confirmation link is invalid or it has expired." msgid "User confirmation link is invalid or it has expired."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:18 #: lib/memex_web/live/invite_live/index.ex:18
#, elixir-autogen, elixir-format
msgid "You are not authorized to view this page" msgid "You are not authorized to view this page"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_auth.ex:177 #: lib/memex_web/controllers/user_auth.ex:177
#, elixir-autogen, elixir-format
msgid "You are not authorized to view this page." msgid "You are not authorized to view this page."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_auth.ex:39 #: lib/memex_web/controllers/user_auth.ex:39
#: lib/memex_web/controllers/user_auth.ex:161 #: lib/memex_web/controllers/user_auth.ex:161
#, elixir-autogen, elixir-format
msgid "You must confirm your account and log in to access this page." msgid "You must confirm your account and log in to access this page."
msgstr "" msgstr ""
#: lib/memex/accounts/user.ex:129
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:130
msgid "did not change" msgid "did not change"
msgstr "" msgstr ""
#: lib/memex/accounts/user.ex:150
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:151
msgid "does not match password" msgid "does not match password"
msgstr "" msgstr ""
#: lib/memex/accounts/user.ex:187
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:188
msgid "is not valid" msgid "is not valid"
msgstr "" msgstr ""
#: lib/memex/accounts/user.ex:85
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:84
msgid "must have the @ sign and no spaces" msgid "must have the @ sign and no spaces"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:21
#: lib/memex_web/templates/user_settings/edit.html.heex:119
#, elixir-autogen, elixir-format
msgid "oops, something went wrong! Please check the errors below"
msgstr ""
#: lib/memex/contexts/context.ex:49
#: lib/memex/contexts/context.ex:60
#: lib/memex/notes/note.ex:48
#: lib/memex/notes/note.ex:59
#: lib/memex/pipelines/pipeline.ex:50
#: lib/memex/pipelines/pipeline.ex:61
#, elixir-autogen, elixir-format
msgid "invalid format: only numbers, letters and hyphen are accepted"
msgstr ""
#: lib/memex_web/templates/error/error.html.heex:28
#, elixir-autogen, elixir-format
msgid "go back home"
msgstr ""
#: lib/memex_web/views/error_view.ex:11
#, elixir-autogen, elixir-format
msgid "internal server error"
msgstr ""
#: lib/memex_web/views/error_view.ex:9
#, elixir-autogen, elixir-format
msgid "not found"
msgstr ""
#: lib/memex_web/views/error_view.ex:10
#, elixir-autogen, elixir-format
msgid "unauthorized"
msgstr ""

View File

@ -10,138 +10,149 @@
msgid "" msgid ""
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:38 #: lib/memex_web/controllers/user_confirmation_controller.ex:38
#, elixir-autogen, elixir-format
msgid "%{email} confirmed successfully." msgid "%{email} confirmed successfully."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.ex:62 #: lib/memex_web/live/invite_live/form_component.ex:62
#, elixir-autogen, elixir-format
msgid "%{invite_name} created successfully" msgid "%{invite_name} created successfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:53 #: lib/memex_web/live/invite_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "%{invite_name} deleted succesfully" msgid "%{invite_name} deleted succesfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:114 #: lib/memex_web/live/invite_live/index.ex:114
#, elixir-autogen, elixir-format
msgid "%{invite_name} disabled succesfully" msgid "%{invite_name} disabled succesfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:90 #: lib/memex_web/live/invite_live/index.ex:90
#, elixir-autogen, elixir-format
msgid "%{invite_name} enabled succesfully" msgid "%{invite_name} enabled succesfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:68 #: lib/memex_web/live/invite_live/index.ex:68
#, elixir-autogen, elixir-format
msgid "%{invite_name} updated succesfully" msgid "%{invite_name} updated succesfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.ex:42 #: lib/memex_web/live/invite_live/form_component.ex:42
#, elixir-autogen, elixir-format
msgid "%{invite_name} updated successfully" msgid "%{invite_name} updated successfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:139 #: lib/memex_web/live/invite_live/index.ex:139
#, elixir-autogen, elixir-format
msgid "%{user_email} deleted succesfully" msgid "%{user_email} deleted succesfully"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:29 #: lib/memex_web/controllers/user_settings_controller.ex:29
#, elixir-autogen, elixir-format
msgid "A link to confirm your email change has been sent to the new address." msgid "A link to confirm your email change has been sent to the new address."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:133
msgid "Are you sure you want to change your language?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:101
#: lib/memex_web/live/invite_live/index.html.heex:130
msgid "Are you sure you want to delete %{email}? This action is permanent!"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:48
msgid "Are you sure you want to delete the invite for %{invite_name}?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:143
msgid "Are you sure you want to delete your account?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:95
msgid "Are you sure you want to log out?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:73
msgid "Are you sure you want to make %{invite_name} unlimited?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:127 #: lib/memex_web/live/invite_live/index.ex:127
#, elixir-autogen, elixir-format
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:77 #: lib/memex_web/controllers/user_settings_controller.ex:77
#, elixir-autogen, elixir-format
msgid "Email changed successfully." msgid "Email changed successfully."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:23 #: lib/memex_web/controllers/user_confirmation_controller.ex:23
#, elixir-autogen, elixir-format
msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly." msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:24 #: lib/memex_web/controllers/user_reset_password_controller.ex:24
#, elixir-autogen, elixir-format
msgid "If your email is in our system, you will receive instructions to reset your password shortly." msgid "If your email is in our system, you will receive instructions to reset your password shortly."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:65 #: lib/memex_web/controllers/user_settings_controller.ex:65
#, elixir-autogen, elixir-format
msgid "Language updated successfully." msgid "Language updated successfully."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_session_controller.ex:23 #: lib/memex_web/controllers/user_session_controller.ex:23
#, elixir-autogen, elixir-format
msgid "Logged out successfully." msgid "Logged out successfully."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:46 #: lib/memex_web/controllers/user_reset_password_controller.ex:46
#, elixir-autogen, elixir-format
msgid "Password reset successfully." msgid "Password reset successfully."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:49 #: lib/memex_web/controllers/user_settings_controller.ex:49
#, elixir-autogen, elixir-format
msgid "Password updated successfully." msgid "Password updated successfully."
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:74
msgid "Please check your email to verify your account" msgid "Please check your email to verify your account"
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:30 #: lib/memex_web/live/invite_live/form_component.html.heex:30
#, elixir-autogen, elixir-format
msgid "Saving..." msgid "Saving..."
msgstr "" msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:95 #: lib/memex_web/controllers/user_settings_controller.ex:95
#, elixir-autogen, elixir-format
msgid "Your account has been deleted" msgid "Your account has been deleted"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:91 msgid "are you sure you want to change your language?"
msgid "Register to setup %{name}" msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:102
#: lib/memex_web/live/invite_live/index.html.heex:132
#, elixir-autogen, elixir-format
msgid "are you sure you want to delete %{email}? This action is permanent!"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:48
#, elixir-autogen, elixir-format
msgid "are you sure you want to delete the invite for %{invite_name}?"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:143
#, elixir-autogen, elixir-format
msgid "are you sure you want to delete your account?"
msgstr ""
#: lib/memex_web/components/topbar.ex:92
#, elixir-autogen, elixir-format
msgid "are you sure you want to log out?"
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:74
#, elixir-autogen, elixir-format
msgid "are you sure you want to make %{invite_name} unlimited?"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:46
#: lib/memex_web/live/context_live/show.html.heex:31
#: lib/memex_web/live/note_live/index.html.heex:46
#: lib/memex_web/live/note_live/show.html.heex:35
#: lib/memex_web/live/pipeline_live/index.html.heex:46
#: lib/memex_web/live/pipeline_live/show.html.heex:40
#: lib/memex_web/live/pipeline_live/show.html.heex:110
#, elixir-autogen, elixir-format
msgid "are you sure?"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:95
#, elixir-autogen, elixir-format
msgid "register to setup %{name}"
msgstr "" msgstr ""

View File

@ -6,10 +6,30 @@ defmodule Memex.Repo.Migrations.CreateContexts do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :title, :string add :title, :string
add :content, :text add :content, :text
add :tag, {:array, :string} add :tags, {:array, :string}
add :visibility, :string add :visibility, :string
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
timestamps() timestamps()
end end
flush()
execute """
ALTER TABLE contexts
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(immutable_array_to_string(tags, ' '), '')), 'B') ||
setweight(to_tsvector('english', coalesce(content, '')), 'C')
) STORED
"""
execute("CREATE INDEX contexts_trgm_idx ON contexts USING GIN (search)")
end
def down do
drop table(:contexts)
end end
end end

View File

@ -6,9 +6,30 @@ defmodule Memex.Repo.Migrations.CreatePipelines do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :title, :string add :title, :string
add :description, :text add :description, :text
add :tags, {:array, :citext}
add :visibility, :string add :visibility, :string
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
timestamps() timestamps()
end end
flush()
execute """
ALTER TABLE pipelines
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(immutable_array_to_string(tags, ' '), '')), 'B') ||
setweight(to_tsvector('english', coalesce(description, '')), 'C')
) STORED
"""
execute("CREATE INDEX pipelines_trgm_idx ON pipelines USING GIN (search)")
end
def down do
drop table(:pipelines)
end end
end end

View File

@ -5,13 +5,16 @@ defmodule Memex.Repo.Migrations.CreateSteps do
create table(:steps, primary_key: false) do create table(:steps, primary_key: false) do
add :id, :binary_id, primary_key: true add :id, :binary_id, primary_key: true
add :title, :string add :title, :string
add :description, :text add :content, :text
add :position, :integer add :position, :integer
add :pipeline_id, references(:pipelines, on_delete: :nothing, type: :binary_id) add :pipeline_id, references(:pipelines, on_delete: :nothing, type: :binary_id)
add :user_id, references(:users, on_delete: :nothing, type: :binary_id)
timestamps() timestamps()
end end
create index(:steps, [:pipeline_id]) create index(:steps, [:pipeline_id])
create index(:steps, [:user_id])
end end
end end

View File

@ -1,16 +0,0 @@
defmodule Memex.Repo.Migrations.CreateStepContexts do
use Ecto.Migration
def change do
create table(:step_contexts, primary_key: false) do
add :id, :binary_id, primary_key: true
add :step_id, references(:steps, on_delete: :nothing, type: :binary_id)
add :context_id, references(:contexts, on_delete: :nothing, type: :binary_id)
timestamps()
end
create index(:step_contexts, [:step_id])
create index(:step_contexts, [:context_id])
end
end

View File

@ -1,16 +0,0 @@
defmodule Memex.Repo.Migrations.CreateContextNotes do
use Ecto.Migration
def change do
create table(:context_notes, primary_key: false) do
add :id, :binary_id, primary_key: true
add :context_id, references(:contexts, on_delete: :nothing, type: :binary_id)
add :note_id, references(:notes, on_delete: :nothing, type: :binary_id)
timestamps()
end
create index(:context_notes, [:context_id])
create index(:context_notes, [:note_id])
end
end

View File

@ -0,0 +1,14 @@
defmodule Memex.Repo.Migrations.UseSlugs do
use Ecto.Migration
def change do
rename table(:notes), :title, to: :slug
create unique_index(:notes, [:slug])
rename table(:contexts), :title, to: :slug
create unique_index(:contexts, [:slug])
rename table(:pipelines), :title, to: :slug
create unique_index(:pipelines, [:slug])
end
end

View File

@ -1,6 +1,8 @@
# Memex # memEx
memex is an easy way to digitize the structured processes of your life. ![old screenshot](https://gitea.bubbletea.dev/shibao/memEx/raw/branch/stable/home.png)
memEx is an easy way to digitize the structured processes of your life.
- Notes: Document notes about individual items or concepts - Notes: Document notes about individual items or concepts
- Contexts: Provide context around a single topic and hotlink to individual - Contexts: Provide context around a single topic and hotlink to individual
@ -16,7 +18,7 @@ memex is an easy way to digitize the structured processes of your life.
# Installation # Installation
1. Install [Docker Compose](https://docs.docker.com/compose/install/) or alternatively [Docker Desktop](https://docs.docker.com/desktop/) on your machine. 1. Install [Docker Compose](https://docs.docker.com/compose/install/) or alternatively [Docker Desktop](https://docs.docker.com/desktop/) on your machine.
1. Copy the example [docker-compose.yml](https://gitea.bubbletea.dev/shibao/memex/src/branch/stable/docker-compose.yml). into your local machine where you want. 1. Copy the example [docker-compose.yml](https://gitea.bubbletea.dev/shibao/memEx/src/branch/stable/docker-compose.yml). into your local machine where you want.
Bind mounts are created in the same directory by default. Bind mounts are created in the same directory by default.
1. Set the configuration variables in `docker-compose.yml`. You'll need to run 1. Set the configuration variables in `docker-compose.yml`. You'll need to run
`docker run -it shibaobun/memex /app/priv/random.sh` to generate a new `docker run -it shibaobun/memex /app/priv/random.sh` to generate a new
@ -27,8 +29,8 @@ The first created user will be created as an admin.
# Configuration # Configuration
You can use the following environment variables to configure Memex in You can use the following environment variables to configure memEx in
[docker-compose.yml](https://gitea.bubbletea.dev/shibao/memex/src/branch/stable/docker-compose.yml). [docker-compose.yml](https://gitea.bubbletea.dev/shibao/memEx/src/branch/stable/docker-compose.yml).
- `HOST`: External url to generate links with. Must be set with your hosted - `HOST`: External url to generate links with. Must be set with your hosted
domain name! I.e. `memex.mywebsite.tld` domain name! I.e. `memex.mywebsite.tld`
@ -55,4 +57,6 @@ You can use the following environment variables to configure Memex in
--- ---
[![Build [![Build
Status](https://drone.bubbletea.dev/api/badges/shibao/memex/status.svg?ref=refs/heads/dev)](https://drone.bubbletea.dev/shibao/memex) Status](https://drone.bubbletea.dev/api/badges/shibao/memEx/status.svg?ref=refs/heads/dev)](https://drone.bubbletea.dev/shibao/memEx)
[![translation
status](https://weblate.bubbletea.dev/widgets/memEx/-/svg-badge.svg)](https://weblate.bubbletea.dev/engage/memEx/)

View File

@ -104,7 +104,7 @@ defmodule Memex.AccountsTest do
describe "change_user_registration/2" do describe "change_user_registration/2" do
test "returns a changeset" do test "returns a changeset" do
assert %Changeset{} = changeset = Accounts.change_user_registration(%User{}) assert %Changeset{} = changeset = Accounts.change_user_registration()
assert changeset.required == [:password, :email] assert changeset.required == [:password, :email]
end end
@ -112,8 +112,7 @@ defmodule Memex.AccountsTest do
email = unique_user_email() email = unique_user_email()
password = valid_user_password() password = valid_user_password()
changeset = changeset = Accounts.change_user_registration(%{"email" => email, "password" => password})
Accounts.change_user_registration(%User{}, %{"email" => email, "password" => password})
assert changeset.valid? assert changeset.valid?
assert get_change(changeset, :email) == email assert get_change(changeset, :email) == email

View File

@ -1,71 +1,138 @@
defmodule Memex.ContextsTest do defmodule Memex.ContextsTest do
use Memex.DataCase use Memex.DataCase
import Memex.ContextsFixtures
alias Memex.Contexts alias Memex.{Contexts, Contexts.Context}
@moduletag :contexts_test
@invalid_attrs %{content: nil, tag: nil, slug: nil, visibility: nil}
describe "contexts" do describe "contexts" do
alias Memex.Contexts.Context setup do
[user: user_fixture()]
import Memex.ContextsFixtures
@invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil}
test "list_contexts/0 returns all contexts" do
context = context_fixture()
assert Contexts.list_contexts() == [context]
end end
test "get_context!/1 returns the context with given id" do test "list_contexts/1 returns all contexts for a user", %{user: user} do
context = context_fixture() context_a = context_fixture(%{slug: "a", visibility: :public}, user)
assert Contexts.get_context!(context.id) == context context_b = context_fixture(%{slug: "b", visibility: :unlisted}, user)
context_c = context_fixture(%{slug: "c", visibility: :private}, user)
assert Contexts.list_contexts(user) == [context_a, context_b, context_c]
end end
test "create_context/1 with valid data creates a context" do test "list_public_contexts/0 returns public contexts", %{user: user} do
valid_attrs = %{content: "some content", tag: [], title: "some title", visibility: :public} public_context = context_fixture(%{visibility: :public}, user)
context_fixture(%{visibility: :unlisted}, user)
context_fixture(%{visibility: :private}, user)
assert Contexts.list_public_contexts() == [public_context]
end
assert {:ok, %Context{} = context} = Contexts.create_context(valid_attrs) test "get_context!/1 returns the context with given id", %{user: user} do
context = context_fixture(%{visibility: :public}, user)
assert Contexts.get_context!(context.id, user) == context
context = context_fixture(%{visibility: :unlisted}, user)
assert Contexts.get_context!(context.id, user) == context
context = context_fixture(%{visibility: :private}, user)
assert Contexts.get_context!(context.id, user) == context
end
test "get_context!/1 only returns unlisted or public contexts for other users", %{user: user} do
another_user = user_fixture()
context = context_fixture(%{visibility: :public}, another_user)
assert Contexts.get_context!(context.id, user) == context
context = context_fixture(%{visibility: :unlisted}, another_user)
assert Contexts.get_context!(context.id, user) == context
context = context_fixture(%{visibility: :private}, another_user)
assert_raise Ecto.NoResultsError, fn ->
Contexts.get_context!(context.id, user)
end
end
test "get_context_by_slug/1 returns the context with given id", %{user: user} do
context = context_fixture(%{slug: "a", visibility: :public}, user)
assert Contexts.get_context_by_slug("a", user) == context
context = context_fixture(%{slug: "b", visibility: :unlisted}, user)
assert Contexts.get_context_by_slug("b", user) == context
context = context_fixture(%{slug: "c", visibility: :private}, user)
assert Contexts.get_context_by_slug("c", user) == context
end
test "get_context_by_slug/1 only returns unlisted or public contexts for other users", %{
user: user
} do
another_user = user_fixture()
context = context_fixture(%{slug: "a", visibility: :public}, another_user)
assert Contexts.get_context_by_slug("a", user) == context
context = context_fixture(%{slug: "b", visibility: :unlisted}, another_user)
assert Contexts.get_context_by_slug("b", user) == context
context_fixture(%{slug: "c", visibility: :private}, another_user)
assert Contexts.get_context_by_slug("c", user) |> is_nil()
end
test "create_context/1 with valid data creates a context", %{user: user} do
valid_attrs = %{
"content" => "some content",
"tags_string" => "tag1,tag2",
"slug" => "some-slug",
"visibility" => :public
}
assert {:ok, %Context{} = context} = Contexts.create_context(valid_attrs, user)
assert context.content == "some content" assert context.content == "some content"
assert context.tag == [] assert context.tags == ["tag1", "tag2"]
assert context.title == "some title" assert context.slug == "some-slug"
assert context.visibility == :public assert context.visibility == :public
end end
test "create_context/1 with invalid data returns error changeset" do test "create_context/1 with invalid data returns error changeset", %{user: user} do
assert {:error, %Ecto.Changeset{}} = Contexts.create_context(@invalid_attrs) assert {:error, %Ecto.Changeset{}} = Contexts.create_context(@invalid_attrs, user)
end end
test "update_context/2 with valid data updates the context" do test "update_context/2 with valid data updates the context", %{user: user} do
context = context_fixture() context = context_fixture(user)
update_attrs = %{ update_attrs = %{
content: "some updated content", "content" => "some updated content",
tag: [], "tags_string" => "tag1,tag2",
title: "some updated title", "slug" => "some-updated-slug",
visibility: :private "visibility" => :private
} }
assert {:ok, %Context{} = context} = Contexts.update_context(context, update_attrs) assert {:ok, %Context{} = context} = Contexts.update_context(context, update_attrs, user)
assert context.content == "some updated content" assert context.content == "some updated content"
assert context.tag == [] assert context.tags == ["tag1", "tag2"]
assert context.title == "some updated title" assert context.slug == "some-updated-slug"
assert context.visibility == :private assert context.visibility == :private
end end
test "update_context/2 with invalid data returns error changeset" do test "update_context/2 with invalid data returns error changeset", %{user: user} do
context = context_fixture() context = context_fixture(user)
assert {:error, %Ecto.Changeset{}} = Contexts.update_context(context, @invalid_attrs) assert {:error, %Ecto.Changeset{}} = Contexts.update_context(context, @invalid_attrs, user)
assert context == Contexts.get_context!(context.id) assert context == Contexts.get_context!(context.id, user)
end end
test "delete_context/1 deletes the context" do test "delete_context/1 deletes the context", %{user: user} do
context = context_fixture() context = context_fixture(user)
assert {:ok, %Context{}} = Contexts.delete_context(context) assert {:ok, %Context{}} = Contexts.delete_context(context, user)
assert_raise Ecto.NoResultsError, fn -> Contexts.get_context!(context.id) end assert_raise Ecto.NoResultsError, fn -> Contexts.get_context!(context.id, user) end
end end
test "change_context/1 returns a context changeset" do test "delete_context/1 deletes the context for an admin user", %{user: user} do
context = context_fixture() admin_user = admin_fixture()
assert %Ecto.Changeset{} = Contexts.change_context(context) context = context_fixture(user)
assert {:ok, %Context{}} = Contexts.delete_context(context, admin_user)
assert_raise Ecto.NoResultsError, fn -> Contexts.get_context!(context.id, user) end
end
test "change_context/1 returns a context changeset", %{user: user} do
context = context_fixture(user)
assert %Ecto.Changeset{} = Contexts.change_context(context, user)
end end
end end
end end

View File

@ -2,8 +2,8 @@ defmodule Memex.NotesTest do
use Memex.DataCase use Memex.DataCase
import Memex.NotesFixtures import Memex.NotesFixtures
alias Memex.{Notes, Notes.Note} alias Memex.{Notes, Notes.Note}
@moduletag :notes_test
@invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil} @invalid_attrs %{content: nil, tag: nil, slug: nil, visibility: nil}
describe "notes" do describe "notes" do
setup do setup do
@ -11,9 +11,9 @@ defmodule Memex.NotesTest do
end end
test "list_notes/1 returns all notes for a user", %{user: user} do test "list_notes/1 returns all notes for a user", %{user: user} do
note_a = note_fixture(%{title: "a", visibility: :public}, user) note_a = note_fixture(%{slug: "a", visibility: :public}, user)
note_b = note_fixture(%{title: "b", visibility: :unlisted}, user) note_b = note_fixture(%{slug: "b", visibility: :unlisted}, user)
note_c = note_fixture(%{title: "c", visibility: :private}, user) note_c = note_fixture(%{slug: "c", visibility: :private}, user)
assert Notes.list_notes(user) == [note_a, note_b, note_c] assert Notes.list_notes(user) == [note_a, note_b, note_c]
end end
@ -25,22 +25,68 @@ defmodule Memex.NotesTest do
end end
test "get_note!/1 returns the note with given id", %{user: user} do test "get_note!/1 returns the note with given id", %{user: user} do
note = note_fixture(user) note = note_fixture(%{visibility: :public}, user)
assert Notes.get_note!(note.id, user) == note assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :unlisted}, user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :private}, user)
assert Notes.get_note!(note.id, user) == note
end
test "get_note!/1 only returns unlisted or public notes for other users", %{user: user} do
another_user = user_fixture()
note = note_fixture(%{visibility: :public}, another_user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :unlisted}, another_user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :private}, another_user)
assert_raise Ecto.NoResultsError, fn ->
Notes.get_note!(note.id, user)
end
end
test "get_note_by_slug/1 returns the note with given id", %{user: user} do
note = note_fixture(%{slug: "a", visibility: :public}, user)
assert Notes.get_note_by_slug("a", user) == note
note = note_fixture(%{slug: "b", visibility: :unlisted}, user)
assert Notes.get_note_by_slug("b", user) == note
note = note_fixture(%{slug: "c", visibility: :private}, user)
assert Notes.get_note_by_slug("c", user) == note
end
test "get_note_by_slug/1 only returns unlisted or public notes for other users", %{
user: user
} do
another_user = user_fixture()
note = note_fixture(%{slug: "a", visibility: :public}, another_user)
assert Notes.get_note_by_slug("a", user) == note
note = note_fixture(%{slug: "b", visibility: :unlisted}, another_user)
assert Notes.get_note_by_slug("b", user) == note
note_fixture(%{slug: "c", visibility: :private}, another_user)
assert Notes.get_note_by_slug("c", user) |> is_nil()
end end
test "create_note/1 with valid data creates a note", %{user: user} do test "create_note/1 with valid data creates a note", %{user: user} do
valid_attrs = %{ valid_attrs = %{
"content" => "some content", "content" => "some content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"title" => "some title", "slug" => "some-slug",
"visibility" => :public "visibility" => :public
} }
assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user) assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user)
assert note.content == "some content" assert note.content == "some content"
assert note.tags == ["tag1", "tag2"] assert note.tags == ["tag1", "tag2"]
assert note.title == "some title" assert note.slug == "some-slug"
assert note.visibility == :public assert note.visibility == :public
end end
@ -54,14 +100,14 @@ defmodule Memex.NotesTest do
update_attrs = %{ update_attrs = %{
"content" => "some updated content", "content" => "some updated content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"title" => "some updated title", "slug" => "some-updated-slug",
"visibility" => :private "visibility" => :private
} }
assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs, user) assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs, user)
assert note.content == "some updated content" assert note.content == "some updated content"
assert note.tags == ["tag1", "tag2"] assert note.tags == ["tag1", "tag2"]
assert note.title == "some updated title" assert note.slug == "some-updated-slug"
assert note.visibility == :private assert note.visibility == :private
end end
@ -77,6 +123,13 @@ defmodule Memex.NotesTest do
assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end
end end
test "delete_note/1 deletes the note for an admin user", %{user: user} do
admin_user = admin_fixture()
note = note_fixture(user)
assert {:ok, %Note{}} = Notes.delete_note(note, admin_user)
assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end
end
test "change_note/1 returns a note changeset", %{user: user} do test "change_note/1 returns a note changeset", %{user: user} do
note = note_fixture(user) note = note_fixture(user)
assert %Ecto.Changeset{} = Notes.change_note(note, user) assert %Ecto.Changeset{} = Notes.change_note(note, user)

View File

@ -1,68 +1,145 @@
defmodule Memex.PipelinesTest do defmodule Memex.PipelinesTest do
use Memex.DataCase use Memex.DataCase
import Memex.PipelinesFixtures
alias Memex.Pipelines alias Memex.{Pipelines, Pipelines.Pipeline}
@moduletag :pipelines_test
@invalid_attrs %{description: nil, tag: nil, slug: nil, visibility: nil}
describe "pipelines" do describe "pipelines" do
alias Memex.Pipelines.Pipeline setup do
[user: user_fixture()]
import Memex.PipelinesFixtures
@invalid_attrs %{description: nil, title: nil, visibility: nil}
test "list_pipelines/0 returns all pipelines" do
pipeline = pipeline_fixture()
assert Pipelines.list_pipelines() == [pipeline]
end end
test "get_pipeline!/1 returns the pipeline with given id" do test "list_pipelines/1 returns all pipelines for a user", %{user: user} do
pipeline = pipeline_fixture() pipeline_a = pipeline_fixture(%{slug: "a", visibility: :public}, user)
assert Pipelines.get_pipeline!(pipeline.id) == pipeline pipeline_b = pipeline_fixture(%{slug: "b", visibility: :unlisted}, user)
pipeline_c = pipeline_fixture(%{slug: "c", visibility: :private}, user)
assert Pipelines.list_pipelines(user) == [pipeline_a, pipeline_b, pipeline_c]
end end
test "create_pipeline/1 with valid data creates a pipeline" do test "list_public_pipelines/0 returns public pipelines", %{user: user} do
valid_attrs = %{description: "some description", title: "some title", visibility: :public} public_pipeline = pipeline_fixture(%{visibility: :public}, user)
pipeline_fixture(%{visibility: :unlisted}, user)
pipeline_fixture(%{visibility: :private}, user)
assert Pipelines.list_public_pipelines() == [public_pipeline]
end
assert {:ok, %Pipeline{} = pipeline} = Pipelines.create_pipeline(valid_attrs) test "get_pipeline!/1 returns the pipeline with given id", %{user: user} do
pipeline = pipeline_fixture(%{visibility: :public}, user)
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
pipeline = pipeline_fixture(%{visibility: :unlisted}, user)
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
pipeline = pipeline_fixture(%{visibility: :private}, user)
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
end
test "get_pipeline!/1 only returns unlisted or public pipelines for other users", %{
user: user
} do
another_user = user_fixture()
pipeline = pipeline_fixture(%{visibility: :public}, another_user)
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
pipeline = pipeline_fixture(%{visibility: :unlisted}, another_user)
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
pipeline = pipeline_fixture(%{visibility: :private}, another_user)
assert_raise Ecto.NoResultsError, fn ->
Pipelines.get_pipeline!(pipeline.id, user)
end
end
test "get_pipeline_by_slug/1 returns the pipeline with given id", %{user: user} do
pipeline = pipeline_fixture(%{slug: "a", visibility: :public}, user)
assert Pipelines.get_pipeline_by_slug("a", user) == pipeline
pipeline = pipeline_fixture(%{slug: "b", visibility: :unlisted}, user)
assert Pipelines.get_pipeline_by_slug("b", user) == pipeline
pipeline = pipeline_fixture(%{slug: "c", visibility: :private}, user)
assert Pipelines.get_pipeline_by_slug("c", user) == pipeline
end
test "get_pipeline_by_slug/1 only returns unlisted or public pipelines for other users", %{
user: user
} do
another_user = user_fixture()
pipeline = pipeline_fixture(%{slug: "a", visibility: :public}, another_user)
assert Pipelines.get_pipeline_by_slug("a", user) == pipeline
pipeline = pipeline_fixture(%{slug: "b", visibility: :unlisted}, another_user)
assert Pipelines.get_pipeline_by_slug("b", user) == pipeline
pipeline_fixture(%{slug: "c", visibility: :private}, another_user)
assert Pipelines.get_pipeline_by_slug("c", user) |> is_nil()
end
test "create_pipeline/1 with valid data creates a pipeline", %{user: user} do
valid_attrs = %{
"description" => "some description",
"tags_string" => "tag1,tag2",
"slug" => "some-slug",
"visibility" => :public
}
assert {:ok, %Pipeline{} = pipeline} = Pipelines.create_pipeline(valid_attrs, user)
assert pipeline.description == "some description" assert pipeline.description == "some description"
assert pipeline.title == "some title" assert pipeline.tags == ["tag1", "tag2"]
assert pipeline.slug == "some-slug"
assert pipeline.visibility == :public assert pipeline.visibility == :public
end end
test "create_pipeline/1 with invalid data returns error changeset" do test "create_pipeline/1 with invalid data returns error changeset", %{user: user} do
assert {:error, %Ecto.Changeset{}} = Pipelines.create_pipeline(@invalid_attrs) assert {:error, %Ecto.Changeset{}} = Pipelines.create_pipeline(@invalid_attrs, user)
end end
test "update_pipeline/2 with valid data updates the pipeline" do test "update_pipeline/2 with valid data updates the pipeline", %{user: user} do
pipeline = pipeline_fixture() pipeline = pipeline_fixture(user)
update_attrs = %{ update_attrs = %{
description: "some updated description", "description" => "some updated description",
title: "some updated title", "tags_string" => "tag1,tag2",
visibility: :private "slug" => "some-updated-slug",
"visibility" => :private
} }
assert {:ok, %Pipeline{} = pipeline} = Pipelines.update_pipeline(pipeline, update_attrs) assert {:ok, %Pipeline{} = pipeline} =
Pipelines.update_pipeline(pipeline, update_attrs, user)
assert pipeline.description == "some updated description" assert pipeline.description == "some updated description"
assert pipeline.title == "some updated title" assert pipeline.tags == ["tag1", "tag2"]
assert pipeline.slug == "some-updated-slug"
assert pipeline.visibility == :private assert pipeline.visibility == :private
end end
test "update_pipeline/2 with invalid data returns error changeset" do test "update_pipeline/2 with invalid data returns error changeset", %{user: user} do
pipeline = pipeline_fixture() pipeline = pipeline_fixture(user)
assert {:error, %Ecto.Changeset{}} = Pipelines.update_pipeline(pipeline, @invalid_attrs)
assert pipeline == Pipelines.get_pipeline!(pipeline.id) assert {:error, %Ecto.Changeset{}} =
Pipelines.update_pipeline(pipeline, @invalid_attrs, user)
assert pipeline == Pipelines.get_pipeline!(pipeline.id, user)
end end
test "delete_pipeline/1 deletes the pipeline" do test "delete_pipeline/1 deletes the pipeline", %{user: user} do
pipeline = pipeline_fixture() pipeline = pipeline_fixture(user)
assert {:ok, %Pipeline{}} = Pipelines.delete_pipeline(pipeline) assert {:ok, %Pipeline{}} = Pipelines.delete_pipeline(pipeline, user)
assert_raise Ecto.NoResultsError, fn -> Pipelines.get_pipeline!(pipeline.id) end assert_raise Ecto.NoResultsError, fn -> Pipelines.get_pipeline!(pipeline.id, user) end
end end
test "change_pipeline/1 returns a pipeline changeset" do test "delete_pipeline/1 deletes the pipeline for an admin user", %{user: user} do
pipeline = pipeline_fixture() admin_user = admin_fixture()
assert %Ecto.Changeset{} = Pipelines.change_pipeline(pipeline) pipeline = pipeline_fixture(user)
assert {:ok, %Pipeline{}} = Pipelines.delete_pipeline(pipeline, admin_user)
assert_raise Ecto.NoResultsError, fn -> Pipelines.get_pipeline!(pipeline.id, user) end
end
test "change_pipeline/1 returns a pipeline changeset", %{user: user} do
pipeline = pipeline_fixture(user)
assert %Ecto.Changeset{} = Pipelines.change_pipeline(pipeline, user)
end end
end end
end end

View File

@ -1,68 +1,148 @@
defmodule Memex.StepsTest do defmodule Memex.StepsTest do
use Memex.DataCase use Memex.DataCase
import Memex.{PipelinesFixtures, StepsFixtures}
alias Memex.Steps alias Memex.Pipelines.{Steps, Steps.Step}
@moduletag :steps_test
@invalid_attrs %{content: nil, title: nil}
describe "steps" do describe "steps" do
alias Memex.Steps.Step setup do
user = user_fixture()
pipeline = pipeline_fixture(user)
import Memex.StepsFixtures [user: user, pipeline: pipeline]
@invalid_attrs %{description: nil, position: nil, title: nil}
test "list_steps/0 returns all steps" do
step = step_fixture()
assert Steps.list_steps() == [step]
end end
test "get_step!/1 returns the step with given id" do test "list_steps/2 returns all steps for a user", %{pipeline: pipeline, user: user} do
step = step_fixture() step_a = step_fixture(0, pipeline, user)
assert Steps.get_step!(step.id) == step step_b = step_fixture(1, pipeline, user)
step_c = step_fixture(2, pipeline, user)
assert Steps.list_steps(pipeline, user) == [step_a, step_b, step_c]
end end
test "create_step/1 with valid data creates a step" do test "get_step!/2 returns the step with given id", %{pipeline: pipeline, user: user} do
valid_attrs = %{description: "some description", position: 42, title: "some title"} step = step_fixture(0, pipeline, user)
assert Steps.get_step!(step.id, user) == step
end
assert {:ok, %Step{} = step} = Steps.create_step(valid_attrs) test "get_step!/2 only returns unlisted or public steps for other users", %{user: user} do
assert step.description == "some description" another_user = user_fixture()
assert step.position == 42 another_pipeline = pipeline_fixture(another_user)
step = step_fixture(0, another_pipeline, another_user)
assert_raise Ecto.NoResultsError, fn ->
Steps.get_step!(step.id, user)
end
end
test "create_step/4 with valid data creates a step", %{pipeline: pipeline, user: user} do
valid_attrs = %{
"content" => "some content",
"title" => "some title"
}
assert {:ok, %Step{} = step} = Steps.create_step(valid_attrs, 0, pipeline, user)
assert step.content == "some content"
assert step.title == "some title" assert step.title == "some title"
end end
test "create_step/1 with invalid data returns error changeset" do test "create_step/4 with invalid data returns error changeset",
assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs) %{pipeline: pipeline, user: user} do
assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs, 0, pipeline, user)
end end
test "update_step/2 with valid data updates the step" do test "update_step/3 with valid data updates the step", %{pipeline: pipeline, user: user} do
step = step_fixture() step = step_fixture(0, pipeline, user)
update_attrs = %{ update_attrs = %{
description: "some updated description", "content" => "some updated content",
position: 43, "title" => "some updated title"
title: "some updated title"
} }
assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs) assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs, user)
assert step.description == "some updated description" assert step.content == "some updated content"
assert step.position == 43
assert step.title == "some updated title" assert step.title == "some updated title"
end end
test "update_step/2 with invalid data returns error changeset" do test "update_step/3 with invalid data returns error changeset", %{
step = step_fixture() pipeline: pipeline,
assert {:error, %Ecto.Changeset{}} = Steps.update_step(step, @invalid_attrs) user: user
assert step == Steps.get_step!(step.id) } do
step = step_fixture(0, pipeline, user)
assert {:error, %Ecto.Changeset{}} = Steps.update_step(step, @invalid_attrs, user)
assert step == Steps.get_step!(step.id, user)
end end
test "delete_step/1 deletes the step" do test "delete_step/2 deletes the step", %{pipeline: pipeline, user: user} do
step = step_fixture() step = step_fixture(0, pipeline, user)
assert {:ok, %Step{}} = Steps.delete_step(step) assert {:ok, %Step{}} = Steps.delete_step(step, user)
assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id) end assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id, user) end
end end
test "change_step/1 returns a step changeset" do test "delete_step/2 moves past steps up", %{pipeline: pipeline, user: user} do
step = step_fixture() first_step = step_fixture(0, pipeline, user)
assert %Ecto.Changeset{} = Steps.change_step(step) second_step = step_fixture(1, pipeline, user)
assert {:ok, %Step{}} = Steps.delete_step(first_step, user)
assert %{position: 0} = second_step |> Repo.reload!()
end
test "delete_step/2 deletes the step for an admin user", %{pipeline: pipeline, user: user} do
admin_user = admin_fixture()
step = step_fixture(0, pipeline, user)
assert {:ok, %Step{}} = Steps.delete_step(step, admin_user)
assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id, user) end
end
test "change_step/2 returns a step changeset", %{pipeline: pipeline, user: user} do
step = step_fixture(0, pipeline, user)
assert %Ecto.Changeset{} = Steps.change_step(step, user)
end
test "change_step/1 returns a step changeset", %{pipeline: pipeline, user: user} do
step = step_fixture(0, pipeline, user)
assert %Ecto.Changeset{} = Steps.change_step(step, user)
end
test "reorder_step/1 reorders steps properly", %{pipeline: pipeline, user: user} do
[
%{id: first_step_id} = first_step,
%{id: second_step_id} = second_step,
%{id: third_step_id} = third_step
] = Enum.map(0..2, fn index -> step_fixture(index, pipeline, user) end)
Steps.reorder_step(third_step, :up, user)
assert [
%{id: ^first_step_id, position: 0},
%{id: ^third_step_id, position: 1},
%{id: ^second_step_id, position: 2}
] = Steps.list_steps(pipeline, user)
Steps.reorder_step(first_step, :up, user)
assert [
%{id: ^first_step_id, position: 0},
%{id: ^third_step_id, position: 1},
%{id: ^second_step_id, position: 2}
] = Steps.list_steps(pipeline, user)
second_step
|> Repo.reload!()
|> Steps.reorder_step(:down, user)
assert [
%{id: ^first_step_id, position: 0},
%{id: ^third_step_id, position: 1},
%{id: ^second_step_id, position: 2}
] = Steps.list_steps(pipeline, user)
Steps.reorder_step(first_step, :down, user)
assert [
%{id: ^third_step_id, position: 0},
%{id: ^first_step_id, position: 1},
%{id: ^second_step_id, position: 2}
] = Steps.list_steps(pipeline, user)
end end
end end
end end

View File

@ -9,6 +9,6 @@ defmodule MemexWeb.HomeControllerTest do
test "GET /", %{conn: conn} do test "GET /", %{conn: conn} do
conn = get(conn, "/") conn = get(conn, "/")
assert html_response(conn, 200) =~ "memex" assert html_response(conn, 200) =~ "memEx"
end end
end end

View File

@ -1,40 +1,39 @@
defmodule MemexWeb.ContextLiveTest do defmodule MemexWeb.ContextLiveTest do
use MemexWeb.ConnCase use MemexWeb.ConnCase
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
import Memex.ContextsFixtures import Memex.{ContextsFixtures, NotesFixtures}
alias MemexWeb.Endpoint
@create_attrs %{ @create_attrs %{
"content" => "some content", "content" => "some content",
"tags_string" => "tag1", "tags_string" => "tag1",
"title" => "some title", "slug" => "some-slug",
"visibility" => :public "visibility" => :public
} }
@update_attrs %{ @update_attrs %{
"content" => "some updated content", "content" => "some updated content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"title" => "some updated title", "slug" => "some-updated-slug",
"visibility" => :private "visibility" => :private
} }
@invalid_attrs %{ @invalid_attrs %{
"content" => nil, "content" => nil,
"tags_string" => "", "tags_string" => "",
"title" => nil, "slug" => nil,
"visibility" => nil "visibility" => nil
} }
defp create_context(_) do defp create_context(%{user: user}) do
context = context_fixture() [context: context_fixture(user)]
%{context: context}
end end
describe "Index" do describe "Index" do
setup [:create_context] setup [:register_and_log_in_user, :create_context]
test "lists all contexts", %{conn: conn, context: context} do test "lists all contexts", %{conn: conn, context: context} do
{:ok, _index_live, html} = live(conn, Routes.context_index_path(conn, :index)) {:ok, _index_live, html} = live(conn, Routes.context_index_path(conn, :index))
assert html =~ "listing contexts" assert html =~ "contexts"
assert html =~ context.content assert html =~ context.content
end end
@ -56,17 +55,17 @@ defmodule MemexWeb.ContextLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.context_index_path(conn, :index)) |> follow_redirect(conn, Routes.context_index_path(conn, :index))
assert html =~ "context created successfully" assert html =~ "#{@create_attrs |> Map.get("slug")} created"
assert html =~ "some content" assert html =~ "some content"
end end
test "updates context in listing", %{conn: conn, context: context} do test "updates context in listing", %{conn: conn, context: context} do
{:ok, index_live, _html} = live(conn, Routes.context_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, Routes.context_index_path(conn, :index))
assert index_live |> element("#context-#{context.id} a", "edit") |> render_click() =~ assert index_live |> element("[data-qa=\"context-edit-#{context.id}\"]") |> render_click() =~
"edit context" "edit"
assert_patch(index_live, Routes.context_index_path(conn, :edit, context)) assert_patch(index_live, Routes.context_index_path(conn, :edit, context.slug))
assert index_live assert index_live
|> form("#context-form", context: @invalid_attrs) |> form("#context-form", context: @invalid_attrs)
@ -78,35 +77,34 @@ defmodule MemexWeb.ContextLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.context_index_path(conn, :index)) |> follow_redirect(conn, Routes.context_index_path(conn, :index))
assert html =~ "context updated successfully" assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
assert html =~ "some updated content" assert html =~ "some updated content"
end end
test "deletes context in listing", %{conn: conn, context: context} do test "deletes context in listing", %{conn: conn, context: context} do
{:ok, index_live, _html} = live(conn, Routes.context_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, Routes.context_index_path(conn, :index))
assert index_live |> element("#context-#{context.id} a", "delete") |> render_click() assert index_live |> element("[data-qa=\"delete-context-#{context.id}\"]") |> render_click()
refute has_element?(index_live, "#context-#{context.id}") refute has_element?(index_live, "#context-#{context.id}")
end end
end end
describe "show" do describe "show" do
setup [:create_context] setup [:register_and_log_in_user, :create_context]
test "displays context", %{conn: conn, context: context} do test "displays context", %{conn: conn, context: context} do
{:ok, _show_live, html} = live(conn, Routes.context_show_path(conn, :show, context)) {:ok, _show_live, html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
assert html =~ "show context" assert html =~ "context"
assert html =~ context.content assert html =~ context.content
end end
test "updates context within modal", %{conn: conn, context: context} do test "updates context within modal", %{conn: conn, context: context} do
{:ok, show_live, _html} = live(conn, Routes.context_show_path(conn, :show, context)) {:ok, show_live, _html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
assert show_live |> element("a", "edit") |> render_click() =~ assert show_live |> element("a", "edit") |> render_click() =~ "edit"
"edit context"
assert_patch(show_live, Routes.context_show_path(conn, :edit, context)) assert_patch(show_live, Routes.context_show_path(conn, :edit, context.slug))
assert show_live assert show_live
|> form("#context-form", context: @invalid_attrs) |> form("#context-form", context: @invalid_attrs)
@ -114,12 +112,46 @@ defmodule MemexWeb.ContextLiveTest do
{:ok, _, html} = {:ok, _, html} =
show_live show_live
|> form("#context-form", context: @update_attrs) |> form("#context-form", context: Map.put(@update_attrs, "slug", context.slug))
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.context_show_path(conn, :show, context)) |> follow_redirect(conn, Routes.context_show_path(conn, :show, context.slug))
assert html =~ "context updated successfully" assert html =~ "#{context.slug} saved"
assert html =~ "some updated content" assert html =~ "some updated content"
end end
test "deletes context", %{conn: conn, context: context} do
{:ok, show_live, _html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
{:ok, index_live, _html} =
show_live
|> element("[data-qa=\"delete-context-#{context.id}\"]")
|> render_click()
|> follow_redirect(conn, Routes.context_index_path(conn, :index))
refute has_element?(index_live, "#context-#{context.id}")
end
end
describe "show with note" do
setup [:register_and_log_in_user]
setup %{user: user} do
%{slug: note_slug} = note = note_fixture(user)
[
note: note,
context:
context_fixture(%{content: "example with backlink to [[#{note_slug}]] note"}, user)
]
end
test "displays context", %{conn: conn, context: context, note: %{slug: note_slug}} do
{:ok, show_live, html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
assert html =~ "context"
assert html =~ Routes.note_show_path(Endpoint, :show, note_slug)
assert has_element?(show_live, "[data-qa=\"context-note-#{note_slug}\"]")
end
end end
end end

View File

@ -7,19 +7,19 @@ defmodule MemexWeb.NoteLiveTest do
@create_attrs %{ @create_attrs %{
"content" => "some content", "content" => "some content",
"tags_string" => "tag1", "tags_string" => "tag1",
"title" => "some title", "slug" => "some-slug",
"visibility" => :public "visibility" => :public
} }
@update_attrs %{ @update_attrs %{
"content" => "some updated content", "content" => "some updated content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"title" => "some updated title", "slug" => "some-updated-slug",
"visibility" => :private "visibility" => :private
} }
@invalid_attrs %{ @invalid_attrs %{
"content" => nil, "content" => nil,
"tags_string" => "", "tags_string" => "",
"title" => nil, "slug" => nil,
"visibility" => nil "visibility" => nil
} }
@ -55,7 +55,7 @@ defmodule MemexWeb.NoteLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.note_index_path(conn, :index)) |> follow_redirect(conn, Routes.note_index_path(conn, :index))
assert html =~ "#{@create_attrs |> Map.get("title")} created" assert html =~ "#{@create_attrs |> Map.get("slug")} created"
assert html =~ "some content" assert html =~ "some content"
end end
@ -65,7 +65,7 @@ defmodule MemexWeb.NoteLiveTest do
assert index_live |> element("[data-qa=\"note-edit-#{note.id}\"]") |> render_click() =~ assert index_live |> element("[data-qa=\"note-edit-#{note.id}\"]") |> render_click() =~
"edit" "edit"
assert_patch(index_live, Routes.note_index_path(conn, :edit, note)) assert_patch(index_live, Routes.note_index_path(conn, :edit, note.slug))
assert index_live assert index_live
|> form("#note-form", note: @invalid_attrs) |> form("#note-form", note: @invalid_attrs)
@ -77,7 +77,7 @@ defmodule MemexWeb.NoteLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.note_index_path(conn, :index)) |> follow_redirect(conn, Routes.note_index_path(conn, :index))
assert html =~ "#{@update_attrs |> Map.get("title")} saved" assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
assert html =~ "some updated content" assert html =~ "some updated content"
end end
@ -93,18 +93,18 @@ defmodule MemexWeb.NoteLiveTest do
setup [:register_and_log_in_user, :create_note] setup [:register_and_log_in_user, :create_note]
test "displays note", %{conn: conn, note: note} do test "displays note", %{conn: conn, note: note} do
{:ok, _show_live, html} = live(conn, Routes.note_show_path(conn, :show, note)) {:ok, _show_live, html} = live(conn, Routes.note_show_path(conn, :show, note.slug))
assert html =~ "show note" assert html =~ "note"
assert html =~ note.content assert html =~ note.content
end end
test "updates note within modal", %{conn: conn, note: note} do test "updates note within modal", %{conn: conn, note: note} do
{:ok, show_live, _html} = live(conn, Routes.note_show_path(conn, :show, note)) {:ok, show_live, _html} = live(conn, Routes.note_show_path(conn, :show, note.slug))
assert show_live |> element("a", "edit") |> render_click() =~ "edit" assert show_live |> element("a", "edit") |> render_click() =~ "edit"
assert_patch(show_live, Routes.note_show_path(conn, :edit, note)) assert_patch(show_live, Routes.note_show_path(conn, :edit, note.slug))
assert show_live assert show_live
|> form("#note-form", note: @invalid_attrs) |> form("#note-form", note: @invalid_attrs)
@ -112,12 +112,24 @@ defmodule MemexWeb.NoteLiveTest do
{:ok, _, html} = {:ok, _, html} =
show_live show_live
|> form("#note-form", note: @update_attrs) |> form("#note-form", note: Map.put(@update_attrs, "slug", note.slug))
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.note_show_path(conn, :show, note)) |> follow_redirect(conn, Routes.note_show_path(conn, :show, note.slug))
assert html =~ "#{@update_attrs |> Map.get("title")} saved" assert html =~ "#{note.slug} saved"
assert html =~ "some updated content" assert html =~ "some updated content"
end end
test "deletes note", %{conn: conn, note: note} do
{:ok, show_live, _html} = live(conn, Routes.note_show_path(conn, :show, note.slug))
{:ok, index_live, _html} =
show_live
|> element("[data-qa=\"delete-note-#{note.id}\"]")
|> render_click()
|> follow_redirect(conn, Routes.note_index_path(conn, :index))
refute has_element?(index_live, "#note-#{note.id}")
end
end end
end end

View File

@ -5,7 +5,7 @@ defmodule MemexWeb.HomeLiveTest do
test "disconnected and connected render", %{conn: conn} do test "disconnected and connected render", %{conn: conn} do
{:ok, page_live, disconnected_html} = live(conn, "/") {:ok, page_live, disconnected_html} = live(conn, "/")
assert disconnected_html =~ "memex" assert disconnected_html =~ "memEx"
assert render(page_live) =~ "memex" assert render(page_live) =~ "memEx"
end end
end end

View File

@ -1,29 +1,50 @@
defmodule MemexWeb.PipelineLiveTest do defmodule MemexWeb.PipelineLiveTest do
use MemexWeb.ConnCase use MemexWeb.ConnCase
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
import Memex.PipelinesFixtures import Memex.{PipelinesFixtures, StepsFixtures}
@create_attrs %{description: "some description", title: "some title", visibility: :public} @create_attrs %{
@update_attrs %{ "description" => "some description",
description: "some updated description", "tags_string" => "tag1",
title: "some updated title", "slug" => "some-slug",
visibility: :private "visibility" => :public
}
@update_attrs %{
"description" => "some updated description",
"tags_string" => "tag1,tag2",
"slug" => "some-updated-slug",
"visibility" => :private
}
@invalid_attrs %{
"description" => nil,
"tags_string" => "",
"slug" => nil,
"visibility" => nil
}
@step_create_attrs %{
"content" => "some content",
"title" => "some title"
}
@step_update_attrs %{
"content" => "some updated content",
"title" => "some updated title"
}
@step_invalid_attrs %{
"content" => nil,
"title" => nil
} }
@invalid_attrs %{description: nil, title: nil, visibility: nil}
defp create_pipeline(_) do defp create_pipeline(%{user: user}) do
pipeline = pipeline_fixture() [pipeline: pipeline_fixture(user)]
%{pipeline: pipeline}
end end
describe "Index" do describe "Index" do
setup [:create_pipeline] setup [:register_and_log_in_user, :create_pipeline]
test "lists all pipelines", %{conn: conn, pipeline: pipeline} do test "lists all pipelines", %{conn: conn, pipeline: pipeline} do
{:ok, _index_live, html} = live(conn, Routes.pipeline_index_path(conn, :index)) {:ok, _index_live, html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "listing pipelines" assert html =~ "pipelines"
assert html =~ pipeline.description assert html =~ pipeline.description
end end
@ -45,17 +66,17 @@ defmodule MemexWeb.PipelineLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index)) |> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "pipeline created successfully" assert html =~ "#{@create_attrs |> Map.get("slug")} created"
assert html =~ "some description" assert html =~ "some description"
end end
test "updates pipeline in listing", %{conn: conn, pipeline: pipeline} do test "updates pipeline in listing", %{conn: conn, pipeline: pipeline} do
{:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert index_live |> element("#pipeline-#{pipeline.id} a", "edit") |> render_click() =~ assert index_live |> element("[data-qa=\"pipeline-edit-#{pipeline.id}\"]") |> render_click() =~
"edit pipeline" "edit"
assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline)) assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline.slug))
assert index_live assert index_live
|> form("#pipeline-form", pipeline: @invalid_attrs) |> form("#pipeline-form", pipeline: @invalid_attrs)
@ -67,35 +88,37 @@ defmodule MemexWeb.PipelineLiveTest do
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index)) |> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "pipeline updated successfully" assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
assert html =~ "some updated description" assert html =~ "some updated description"
end end
test "deletes pipeline in listing", %{conn: conn, pipeline: pipeline} do test "deletes pipeline in listing", %{conn: conn, pipeline: pipeline} do
{:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index)) {:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert index_live |> element("#pipeline-#{pipeline.id} a", "delete") |> render_click() assert index_live
|> element("[data-qa=\"delete-pipeline-#{pipeline.id}\"]")
|> render_click()
refute has_element?(index_live, "#pipeline-#{pipeline.id}") refute has_element?(index_live, "#pipeline-#{pipeline.id}")
end end
end end
describe "show" do describe "show" do
setup [:create_pipeline] setup [:register_and_log_in_user, :create_pipeline]
test "displays pipeline", %{conn: conn, pipeline: pipeline} do test "displays pipeline", %{conn: conn, pipeline: pipeline} do
{:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline)) {:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert html =~ "show pipeline" assert html =~ "pipeline"
assert html =~ pipeline.description assert html =~ pipeline.description
end end
test "updates pipeline within modal", %{conn: conn, pipeline: pipeline} do test "updates pipeline within modal", %{conn: conn, pipeline: pipeline} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline)) {:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert show_live |> element("a", "edit") |> render_click() =~ assert show_live |> element("a", "edit") |> render_click() =~ "edit"
"edit pipeline"
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline)) assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline.slug))
assert show_live assert show_live
|> form("#pipeline-form", pipeline: @invalid_attrs) |> form("#pipeline-form", pipeline: @invalid_attrs)
@ -103,12 +126,129 @@ defmodule MemexWeb.PipelineLiveTest do
{:ok, _, html} = {:ok, _, html} =
show_live show_live
|> form("#pipeline-form", pipeline: @update_attrs) |> form("#pipeline-form", pipeline: Map.put(@update_attrs, "slug", pipeline.slug))
|> render_submit() |> render_submit()
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline)) |> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert html =~ "pipeline updated successfully" assert html =~ "#{pipeline.slug} saved"
assert html =~ "some updated description" assert html =~ "some updated description"
end end
test "deletes pipeline", %{conn: conn, pipeline: pipeline} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
{:ok, index_live, _html} =
show_live
|> element("[data-qa=\"delete-pipeline-#{pipeline.id}\"]")
|> render_click()
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
refute has_element?(index_live, "#pipeline-#{pipeline.id}")
end
test "creates a step", %{conn: conn, pipeline: pipeline} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
show_live
|> element("[data-qa=\"add-step-#{pipeline.id}\"]")
|> render_click()
assert_patch(show_live, Routes.pipeline_show_path(conn, :add_step, pipeline.slug))
{:ok, _show_live, html} =
show_live
|> form("#step-form", step: @step_create_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert html =~ "some title created"
assert html =~ "some description"
end
end
describe "show with a step" do
setup [:register_and_log_in_user, :create_pipeline]
setup %{pipeline: pipeline, user: user} do
[
step: step_fixture(0, pipeline, user)
]
end
test "updates a step", %{conn: conn, pipeline: pipeline, step: step} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
show_live
|> element("[data-qa=\"edit-step-#{step.id}\"]")
|> render_click()
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit_step, pipeline.slug, step.id))
assert show_live
|> form("#step-form", step: @step_invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
{:ok, _show_live, html} =
show_live
|> form("#step-form", step: @step_update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert html =~ "some updated title saved"
assert html =~ "some updated content"
end
test "deletes a step", %{conn: conn, pipeline: pipeline, step: step} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
html =
show_live
|> element("[data-qa=\"delete-step-#{step.id}\"]")
|> render_click()
assert_patch(show_live, Routes.pipeline_show_path(conn, :show, pipeline.slug))
assert html =~ "some title deleted"
refute html =~ "some updated content"
end
end
describe "show with multiple steps" do
setup [:register_and_log_in_user, :create_pipeline]
setup %{pipeline: pipeline, user: user} do
[
first_step: step_fixture(%{title: "first step"}, 0, pipeline, user),
second_step: step_fixture(%{title: "second step"}, 1, pipeline, user),
third_step: step_fixture(%{title: "third step"}, 2, pipeline, user)
]
end
test "reorders a step",
%{conn: conn, pipeline: pipeline, first_step: first_step, second_step: second_step} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
html =
show_live
|> element("[data-qa=\"move-step-up-#{second_step.id}\"]")
|> render_click()
assert html =~ "1. second step"
assert html =~ "2. first step"
assert html =~ "3. third step"
refute has_element?(show_live, "[data-qa=\"move-step-up-#{second_step.id}\"]")
html =
show_live
|> element("[data-qa=\"move-step-down-#{first_step.id}\"]")
|> render_click()
assert html =~ "1. second step"
assert html =~ "2. third step"
assert html =~ "3. first step"
refute has_element?(show_live, "[data-qa=\"move-step-down-#{first_step.id}\"]")
end
end end
end end

View File

@ -13,11 +13,11 @@ defmodule MemexWeb.ErrorViewTest do
test "renders 404.html" do test "renders 404.html" do
assert render_to_string(MemexWeb.ErrorView, "404.html", []) =~ assert render_to_string(MemexWeb.ErrorView, "404.html", []) =~
dgettext("errors", "Not found") dgettext("errors", "not found")
end end
test "renders 500.html" do test "renders 500.html" do
assert render_to_string(MemexWeb.ErrorView, "500.html", []) =~ assert render_to_string(MemexWeb.ErrorView, "500.html", []) =~
dgettext("errors", "Internal Server Error") dgettext("errors", "internal server error")
end end
end end

View File

@ -2,8 +2,7 @@ defmodule Memex.Fixtures do
@moduledoc """ @moduledoc """
This module defines test helpers for creating entities This module defines test helpers for creating entities
""" """
alias Memex.{Accounts, Accounts.User, Email, Repo}
alias Memex.{Accounts, Accounts.User, Email}
def unique_user_email, do: "user#{System.unique_integer()}@example.com" def unique_user_email, do: "user#{System.unique_integer()}@example.com"
def valid_user_password, do: "hello world!" def valid_user_password, do: "hello world!"
@ -26,11 +25,12 @@ defmodule Memex.Fixtures do
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
"email" => unique_user_email(), "email" => unique_user_email(),
"password" => valid_user_password(), "password" => valid_user_password()
"role" => "admin"
}) })
|> Accounts.register_user() |> Accounts.register_user()
|> unwrap_ok_tuple() |> unwrap_ok_tuple()
|> User.role_changeset("admin")
|> Repo.update!()
end end
def extract_user_token(fun) do def extract_user_token(fun) do
@ -56,5 +56,14 @@ defmodule Memex.Fixtures do
}) })
end end
def random_slug(length \\ 20) do
symbols = '0123456789abcdef-'
symbol_count = Enum.count(symbols)
for _ <- Range.new(1, length),
into: "",
do: <<Enum.at(symbols, :rand.uniform(symbol_count - 1))>>
end
defp unwrap_ok_tuple({:ok, value}), do: value defp unwrap_ok_tuple({:ok, value}), do: value
end end

View File

@ -3,20 +3,24 @@ defmodule Memex.ContextsFixtures do
This module defines test helpers for creating This module defines test helpers for creating
entities via the `Memex.Contexts` context. entities via the `Memex.Contexts` context.
""" """
import Memex.Fixtures
alias Memex.{Accounts.User, Contexts, Contexts.Context}
@doc """ @doc """
Generate a context. Generate a context.
""" """
def context_fixture(attrs \\ %{}) do @spec context_fixture(User.t()) :: Context.t()
@spec context_fixture(attrs :: map(), User.t()) :: Context.t()
def context_fixture(attrs \\ %{}, user) do
{:ok, context} = {:ok, context} =
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
content: "some content", content: "some content",
tag: [], tag: [],
title: "some title", slug: random_slug(),
visibility: :public visibility: :private
}) })
|> Memex.Contexts.create_context() |> Contexts.create_context(user)
context context
end end

View File

@ -3,6 +3,7 @@ defmodule Memex.NotesFixtures do
This module defines test helpers for creating This module defines test helpers for creating
entities via the `Memex.Notes` context. entities via the `Memex.Notes` context.
""" """
import Memex.Fixtures
alias Memex.{Accounts.User, Notes, Notes.Note} alias Memex.{Accounts.User, Notes, Notes.Note}
@doc """ @doc """
@ -16,7 +17,7 @@ defmodule Memex.NotesFixtures do
|> Enum.into(%{ |> Enum.into(%{
content: "some content", content: "some content",
tag: [], tag: [],
title: "some title", slug: random_slug(),
visibility: :private visibility: :private
}) })
|> Notes.create_note(user) |> Notes.create_note(user)

View File

@ -3,19 +3,24 @@ defmodule Memex.PipelinesFixtures do
This module defines test helpers for creating This module defines test helpers for creating
entities via the `Memex.Pipelines` context. entities via the `Memex.Pipelines` context.
""" """
import Memex.Fixtures
alias Memex.{Accounts.User, Pipelines, Pipelines.Pipeline}
@doc """ @doc """
Generate a pipeline. Generate a pipeline.
""" """
def pipeline_fixture(attrs \\ %{}) do @spec pipeline_fixture(User.t()) :: Pipeline.t()
@spec pipeline_fixture(attrs :: map(), User.t()) :: Pipeline.t()
def pipeline_fixture(attrs \\ %{}, user) do
{:ok, pipeline} = {:ok, pipeline} =
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
description: "some description", description: "some description",
title: "some title", tag: [],
visibility: :public slug: random_slug(),
visibility: :private
}) })
|> Memex.Pipelines.create_pipeline() |> Pipelines.create_pipeline(user)
pipeline pipeline
end end

View File

@ -3,19 +3,19 @@ defmodule Memex.StepsFixtures do
This module defines test helpers for creating This module defines test helpers for creating
entities via the `Memex.Steps` context. entities via the `Memex.Steps` context.
""" """
alias Memex.Pipelines.Steps
@doc """ @doc """
Generate a step. Generate a step.
""" """
def step_fixture(attrs \\ %{}) do def step_fixture(attrs \\ %{}, position, pipeline, user) do
{:ok, step} = {:ok, step} =
attrs attrs
|> Enum.into(%{ |> Enum.into(%{
description: "some description", content: "some content",
position: 42,
title: "some title" title: "some title"
}) })
|> Memex.Steps.create_step() |> Steps.create_step(position, pipeline, user)
step step
end end