Compare commits

..

5 Commits

Author SHA1 Message Date
c2fb7bac03 add search to notes
Some checks failed
continuous-integration/drone/push Build is failing
2022-11-19 00:21:14 -05:00
ec2fe32afe remove transition all on input 2022-11-19 00:03:13 -05:00
1703661af7 alias endpoint 2022-11-19 00:03:13 -05:00
8d8a556a07 display topbar when user is logged out 2022-11-19 00:03:13 -05:00
30260685e4 work on notes 2022-11-19 00:03:13 -05:00
84 changed files with 1414 additions and 4011 deletions

View File

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

View File

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

View File

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

BIN
home.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

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

View File

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

View File

@ -4,89 +4,21 @@ defmodule Memex.Contexts do
"""
import Ecto.Query, warn: false
alias Ecto.Changeset
alias Memex.{Accounts.User, Contexts.Context, Repo}
alias Memex.Repo
alias Memex.Contexts.Context
@doc """
Returns the list of contexts.
## Examples
iex> list_contexts(%User{id: 123})
iex> list_contexts()
[%Context{}, ...]
iex> list_contexts("my context", %User{id: 123})
[%Context{slug: "my context"}, ...]
"""
@spec list_contexts(User.t()) :: [Context.t()]
@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
)
}
)
def list_contexts do
Repo.all(Context)
end
@doc """
@ -96,78 +28,31 @@ defmodule Memex.Contexts do
## Examples
iex> get_context!(123, %User{id: 123})
iex> get_context!(123)
%Context{}
iex> get_context!(456, %User{id: 123})
iex> get_context!(456)
** (Ecto.NoResultsError)
"""
@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
def get_context!(id), do: Repo.get!(Context, id)
@doc """
Creates a context.
## Examples
iex> create_context(%{field: value}, %User{id: 123})
iex> create_context(%{field: value})
{:ok, %Context{}}
iex> create_context(%{field: bad_value}, %User{id: 123})
iex> create_context(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec create_context(User.t()) :: {:ok, Context.t()} | {:error, Context.changeset()}
@spec create_context(attrs :: map(), User.t()) ::
{:ok, Context.t()} | {:error, Context.changeset()}
def create_context(attrs \\ %{}, user) do
Context.create_changeset(attrs, user) |> Repo.insert()
def create_context(attrs \\ %{}) do
%Context{}
|> Context.changeset(attrs)
|> Repo.insert()
end
@doc """
@ -175,18 +60,16 @@ defmodule Memex.Contexts do
## Examples
iex> update_context(context, %{field: new_value}, %User{id: 123})
iex> update_context(context, %{field: new_value})
{:ok, %Context{}}
iex> update_context(context, %{field: bad_value}, %User{id: 123})
iex> update_context(context, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec update_context(Context.t(), attrs :: map(), User.t()) ::
{:ok, Context.t()} | {:error, Context.changeset()}
def update_context(%Context{} = context, attrs, user) do
def update_context(%Context{} = context, attrs) do
context
|> Context.update_changeset(attrs, user)
|> Context.changeset(attrs)
|> Repo.update()
end
@ -195,24 +78,15 @@ defmodule Memex.Contexts do
## Examples
iex> delete_context(%Context{user_id: 123}, %User{id: 123})
iex> delete_context(context)
{:ok, %Context{}}
iex> delete_context(%Context{user_id: 123}, %User{role: :admin})
{:ok, %Context{}}
iex> delete_context(%Context{}, %User{id: 123})
iex> delete_context(context)
{:error, %Ecto.Changeset{}}
"""
@spec delete_context(Context.t(), User.t()) ::
{: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()
def delete_context(%Context{} = context) do
Repo.delete(context)
end
@doc """
@ -224,23 +98,7 @@ defmodule Memex.Contexts do
%Ecto.Changeset{data: %Context{}}
"""
@spec change_context(Context.t(), User.t()) :: Context.changeset()
@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()
def change_context(%Context{} = context, attrs \\ %{}) do
Context.changeset(context, attrs)
end
end

View File

@ -1,77 +1,22 @@
defmodule Memex.Contexts.Context do
@moduledoc """
Represents a document that synthesizes multiple concepts as defined by notes
into a single consideration
"""
use Ecto.Schema
import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID}
alias Memex.Accounts.User
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "contexts" do
field :slug, :string
field :content, :string
field :tags, {:array, :string}
field :tags_string, :string, virtual: true
field :tag, {:array, :string}
field :title, :string
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User
timestamps()
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
@spec create_changeset(attrs :: map(), User.t()) :: changeset()
def create_changeset(attrs, %User{id: user_id}) do
%__MODULE__{}
|> 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])
def changeset(context, attrs) do
context
|> cast(attrs, [:title, :content, :tag, :visibility])
|> validate_required([:title, :content, :tag, :visibility])
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

View File

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

View File

@ -4,17 +4,16 @@ defmodule Memex.Notes.Note do
"""
use Ecto.Schema
import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID}
alias Memex.Accounts.User
alias Memex.{Accounts.User, Notes.Note}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "notes" do
field :slug, :string
field :content, :string
field :tags, {:array, :string}
field :tags_string, :string, virtual: true
field :title, :string
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User
@ -22,47 +21,29 @@ defmodule Memex.Notes.Note do
timestamps()
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 t :: %Note{}
@type id :: UUID.t()
@type slug :: String.t()
@type changeset :: Changeset.t(t())
@doc false
@spec create_changeset(attrs :: map(), User.t()) :: changeset()
def create_changeset(attrs, %User{id: user_id}) do
%__MODULE__{}
|> cast(attrs, [:slug, :content, :tags, :visibility])
%Note{}
|> cast(attrs, [:title, :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])
|> validate_required([:title, :content, :user_id, :visibility])
end
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
@spec update_changeset(Note.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(attrs, [:title, :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])
|> validate_required([:title, :content, :visibility])
end
defp cast_tags_string(changeset, %{"tags_string" => tags_string})
when tags_string |> is_binary() do
defp cast_tags_string(changeset, %{"tags_string" => tags_string}) when is_binary(tags_string) do
tags =
tags_string
|> String.split(",", trim: true)

View File

@ -4,88 +4,21 @@ defmodule Memex.Pipelines do
"""
import Ecto.Query, warn: false
alias Ecto.Changeset
alias Memex.{Accounts.User, Pipelines.Pipeline, Repo}
alias Memex.Repo
alias Memex.Pipelines.Pipeline
@doc """
Returns the list of pipelines.
## Examples
iex> list_pipelines(%User{id: 123})
iex> list_pipelines()
[%Pipeline{}, ...]
iex> list_pipelines("my pipeline", %User{id: 123})
[%Pipeline{slug: "my pipeline"}, ...]
"""
@spec list_pipelines(User.t()) :: [Pipeline.t()]
@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
)
}
)
def list_pipelines do
Repo.all(Pipeline)
end
@doc """
@ -95,78 +28,31 @@ defmodule Memex.Pipelines do
## Examples
iex> get_pipeline!(123, %User{id: 123})
iex> get_pipeline!(123)
%Pipeline{}
iex> get_pipeline!(456, %User{id: 123})
iex> get_pipeline!(456)
** (Ecto.NoResultsError)
"""
@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
def get_pipeline!(id), do: Repo.get!(Pipeline, id)
@doc """
Creates a pipeline.
## Examples
iex> create_pipeline(%{field: value}, %User{id: 123})
iex> create_pipeline(%{field: value})
{:ok, %Pipeline{}}
iex> create_pipeline(%{field: bad_value}, %User{id: 123})
iex> create_pipeline(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec create_pipeline(User.t()) :: {:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
@spec create_pipeline(attrs :: map(), User.t()) ::
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
def create_pipeline(attrs \\ %{}, user) do
Pipeline.create_changeset(attrs, user) |> Repo.insert()
def create_pipeline(attrs \\ %{}) do
%Pipeline{}
|> Pipeline.changeset(attrs)
|> Repo.insert()
end
@doc """
@ -174,18 +60,16 @@ defmodule Memex.Pipelines do
## Examples
iex> update_pipeline(pipeline, %{field: new_value}, %User{id: 123})
iex> update_pipeline(pipeline, %{field: new_value})
{:ok, %Pipeline{}}
iex> update_pipeline(pipeline, %{field: bad_value}, %User{id: 123})
iex> update_pipeline(pipeline, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec update_pipeline(Pipeline.t(), attrs :: map(), User.t()) ::
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
def update_pipeline(%Pipeline{} = pipeline, attrs, user) do
def update_pipeline(%Pipeline{} = pipeline, attrs) do
pipeline
|> Pipeline.update_changeset(attrs, user)
|> Pipeline.changeset(attrs)
|> Repo.update()
end
@ -194,24 +78,15 @@ defmodule Memex.Pipelines do
## Examples
iex> delete_pipeline(%Pipeline{user_id: 123}, %User{id: 123})
iex> delete_pipeline(pipeline)
{:ok, %Pipeline{}}
iex> delete_pipeline(%Pipeline{}, %User{role: :admin})
{:ok, %Pipeline{}}
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
iex> delete_pipeline(pipeline)
{:error, %Ecto.Changeset{}}
"""
@spec delete_pipeline(Pipeline.t(), User.t()) ::
{: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()
def delete_pipeline(%Pipeline{} = pipeline) do
Repo.delete(pipeline)
end
@doc """
@ -219,30 +94,11 @@ defmodule Memex.Pipelines do
## Examples
iex> change_pipeline(pipeline, %User{id: 123})
%Ecto.Changeset{data: %Pipeline{}}
iex> change_pipeline(pipeline, %{slug: "new slug"}, %User{id: 123})
iex> change_pipeline(pipeline)
%Ecto.Changeset{data: %Pipeline{}}
"""
@spec change_pipeline(Pipeline.t(), User.t()) :: Pipeline.changeset()
@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()
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}) do
Pipeline.changeset(pipeline, attrs)
end
end

View File

@ -1,78 +1,21 @@
defmodule Memex.Pipelines.Pipeline do
@moduledoc """
Represents a chain of considerations to take to accomplish a task
"""
use Ecto.Schema
import Ecto.Changeset
import MemexWeb.Gettext
alias Ecto.{Changeset, UUID}
alias Memex.{Accounts.User, Pipelines.Steps.Step}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "pipelines" do
field :slug, :string
field :description, :string
field :tags, {:array, :string}
field :tags_string, :string, virtual: true
field :title, :string
field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
belongs_to :user, User
has_many :steps, Step, preload_order: [asc: :position]
timestamps()
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
@spec create_changeset(attrs :: map(), User.t()) :: changeset()
def create_changeset(attrs, %User{id: user_id}) do
%__MODULE__{}
|> 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
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
def update_changeset(%{user_id: user_id} = pipeline, attrs, %User{id: user_id}) do
def changeset(pipeline, attrs) 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])
|> cast(attrs, [:title, :description, :visibility])
|> validate_required([:title, :description, :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

View File

@ -1,71 +0,0 @@
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

@ -1,238 +0,0 @@
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

104
lib/memex/steps.ex Normal file
View File

@ -0,0 +1,104 @@
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

22
lib/memex/steps/step.ex Normal file
View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,20 @@
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

@ -1,44 +0,0 @@
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

@ -1,135 +0,0 @@
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

@ -0,0 +1,29 @@
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
columns = [
%{label: gettext("slug"), key: :slug},
%{label: gettext("title"), key: :title},
%{label: gettext("content"), key: :content},
%{label: gettext("tags"), key: :tags},
%{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()) ::
any() | {any(), Rendered.t()}
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
assigns = %{slug: slug}
defp get_value_for_key(:title, %{id: id, title: title}, _additional_data) do
assigns = %{id: id, title: title}
slug_block = ~H"""
title_block = ~H"""
<.link
navigate={Routes.note_show_path(Endpoint, :show, @slug)}
navigate={Routes.note_show_path(Endpoint, :show, @id)}
class="link"
data-qa={"note-show-#{@slug}"}
data-qa={"note-show-#{@id}"}
>
<%= @slug %>
<%= @title %>
</.link>
"""
{slug, slug_block}
{title, title_block}
end
defp get_value_for_key(:content, %{content: content}, _additional_data) do

View File

@ -1,135 +0,0 @@
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

@ -1,44 +0,0 @@
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)}
class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline"
>
<%= gettext("memEx") %>
<%= gettext("memex") %>
</.link>
<%= 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>
<%= if @current_user do %>
<%= if @current_user |> Accounts.is_already_admin?() do %>
<%= if @current_user.role == :admin do %>
<li class="mx-2 my-1">
<.link
navigate={Routes.invite_index_path(Endpoint, :index)}

View File

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

View File

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

View File

@ -1,4 +1,6 @@
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
<div>
<h2><%= @title %></h2>
<.form
:let={f}
for={@changeset}
@ -6,44 +8,27 @@
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, :slug,
class: "input input-primary",
placeholder: gettext("slug")
) %>
<%= error_tag(f, :slug) %>
<%= label(f, :title) %>
<%= text_input(f, :title) %>
<%= error_tag(f, :title) %>
<%= 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")
) %>
<%= label(f, :content) %>
<%= textarea(f, :content) %>
<%= error_tag(f, :content) %>
<%= text_input(f, :tags_string,
id: "tags-input",
class: "input input-primary",
placeholder: gettext("tag1,tag2"),
phx_update: "ignore",
value: Contexts.get_tags_string(@changeset)
<%= label(f, :tag) %>
<%= multiple_select(f, :tag, "Option 1": "option1", "Option 2": "option2") %>
<%= error_tag(f, :tag) %>
<%= label(f, :visibility) %>
<%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
prompt: "Choose a value"
) %>
<%= error_tag(f, :tags_string) %>
<div class="flex justify-center items-stretch space-x-4">
<%= 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>
<%= error_tag(f, :visibility) %>
<div>
<%= submit("Save", phx_disable_with: "Saving...") %>
</div>
</.form>
</div>

View File

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

View File

@ -1,71 +1,10 @@
<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>
<h1>listing contexts</h1>
<%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.context_index_path(@socket, :index)}>
<.live_component
module={MemexWeb.ContextLive.FormComponent}
id={@context.id || :new}
current_user={@current_user}
title={@page_title}
action={@live_action}
context={@context}
@ -73,3 +12,55 @@
/>
</.modal>
<% 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
use MemexWeb, :live_view
import MemexWeb.Components.ContextContent
alias Memex.{Accounts.User, Contexts, Contexts.Context}
alias Memex.Contexts
@impl true
def mount(_params, _session, socket) do
@ -9,50 +9,13 @@ defmodule MemexWeb.ContextLive.Show do
end
@impl true
def handle_params(
%{"slug" => slug},
_,
%{assigns: %{live_action: live_action, current_user: current_user}} = socket
) 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}
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:context, Contexts.get_context!(id))}
end
@impl true
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
defp page_title(:show), do: "show context"
defp page_title(:edit), do: "edit context"
end

View File

@ -1,52 +1,48 @@
<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>
<h1>show context</h1>
<%= if @live_action in [:edit] do %>
<.modal return_to={Routes.context_show_path(@socket, :show, @context.slug)}>
<.modal return_to={Routes.context_show_path(@socket, :show, @context)}>
<.live_component
module={MemexWeb.ContextLive.FormComponent}
id={@context.id}
current_user={@current_user}
title={@page_title}
action={@live_action}
context={@context}
return_to={Routes.context_show_path(@socket, :show, @context.slug)}
return_to={Routes.context_show_path(@socket, :show, @context)}
/>
</.modal>
<% 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

@ -1,12 +0,0 @@
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

@ -1,124 +0,0 @@
<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,11 +5,41 @@ defmodule MemexWeb.HomeLive do
use MemexWeb, :live_view
alias Memex.Accounts
alias MemexWeb.{Endpoint, FaqLive}
@impl true
def mount(_params, _session, socket) do
admins = Accounts.list_users_by_role(:admin)
{:ok, socket |> assign(page_title: gettext("home"), admins: admins)}
{:ok, socket |> assign(page_title: gettext("Home"), query: "", results: %{}, 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
defmodule MemexWeb.NoteLive.Show do
use MemexWeb, :live_view
alias Memex.{Accounts.User, Notes, Notes.Note}
alias Memex.Notes
@impl true
def mount(_params, _session, socket) do
@ -10,49 +10,16 @@ defmodule MemexWeb.NoteLive.Show do
@impl true
def handle_params(
%{"slug" => slug},
%{"id" => id},
_,
%{assigns: %{live_action: live_action, current_user: current_user}} = socket
) do
note =
case Notes.get_note_by_slug(slug, current_user) do
nil -> raise MemexWeb.NotFoundError, gettext("%{slug} could not be found", slug: slug)
note -> note
end
socket =
socket
|> assign(:page_title, page_title(live_action, note))
|> assign(:note, note)
{:noreply, socket}
{:noreply,
socket
|> assign(:page_title, page_title(live_action))
|> assign(:note, Notes.get_note!(id, current_user))}
end
@impl true
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
defp page_title(:show), do: "show note"
defp page_title(:edit), do: "edit note"
end

View File

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

View File

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

View File

@ -1,4 +1,6 @@
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
<div>
<h2><%= @title %></h2>
<.form
:let={f}
for={@changeset}
@ -6,44 +8,23 @@
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, :slug,
class: "input input-primary",
placeholder: gettext("slug")
) %>
<%= error_tag(f, :slug) %>
<%= label(f, :title) %>
<%= text_input(f, :title) %>
<%= error_tag(f, :title) %>
<%= 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")
) %>
<%= label(f, :description) %>
<%= textarea(f, :description) %>
<%= error_tag(f, :description) %>
<%= text_input(f, :tags_string,
id: "tags-input",
class: "input input-primary",
placeholder: gettext("tag1,tag2"),
phx_update: "ignore",
value: Pipelines.get_tags_string(@changeset)
<%= label(f, :visibility) %>
<%= select(f, :visibility, Ecto.Enum.values(Memex.Pipelines.Pipeline, :visibility),
prompt: "Choose a value"
) %>
<%= error_tag(f, :tags_string) %>
<div class="flex justify-center items-stretch space-x-4">
<%= 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>
<%= error_tag(f, :visibility) %>
<div>
<%= submit("Save", phx_disable_with: "Saving...") %>
</div>
</.form>
</div>

View File

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

View File

@ -1,71 +1,10 @@
<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>
<h1>listing pipelines</h1>
<%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.pipeline_index_path(@socket, :index)}>
<.live_component
module={MemexWeb.PipelineLive.FormComponent}
id={@pipeline.id || :new}
current_user={@current_user}
title={@page_title}
action={@live_action}
pipeline={@pipeline}
@ -73,3 +12,53 @@
/>
</.modal>
<% 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,8 +1,7 @@
defmodule MemexWeb.PipelineLive.Show do
use MemexWeb, :live_view
import MemexWeb.Components.StepContent
alias Memex.{Accounts.User, Pipelines}
alias Memex.Pipelines.{Pipeline, Steps, Steps.Step}
alias Memex.Pipelines
@impl true
def mount(_params, _session, socket) do
@ -10,128 +9,13 @@ defmodule MemexWeb.PipelineLive.Show do
end
@impl true
def handle_params(
%{"slug" => slug} = params,
_url,
%{assigns: %{current_user: current_user, live_action: live_action}} = socket
) 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}
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:pipeline, Pipelines.get_pipeline!(id))}
end
defp apply_action(socket, live_action, _params) when live_action in [:show, :edit] do
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
defp page_title(:show), do: "show pipeline"
defp page_title(:edit), do: "edit pipeline"
end

View File

@ -1,174 +1,43 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="text-xl">
<%= @pipeline.slug %>
</h1>
<h1>show pipeline</h1>
<p><%= if @pipeline.tags, do: @pipeline.tags |> Enum.join(", ") %></p>
<%= if @pipeline.description do %>
<textarea
id="show-pipeline-description"
class="input input-primary h-32 min-h-32"
phx-hook="MaintainAttrs"
phx-update="ignore"
readonly
phx-no-format
><%= @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>
<% _ -> %>
<%= if @live_action in [:edit] do %>
<.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)}>
<.live_component
module={MemexWeb.PipelineLive.FormComponent}
id={@pipeline.id}
title={@page_title}
action={@live_action}
pipeline={@pipeline}
return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)}
/>
</.modal>
<% 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

@ -1,74 +0,0 @@
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

@ -1,34 +0,0 @@
<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

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

View File

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

View File

@ -11,7 +11,7 @@
<script defer type="text/javascript" src="/js/app.js">
</script>
</head>
<body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
<body class="pb-8 m-0 p-0 w-full h-full">
<header>
<.topbar current_user={assigns[:current_user]}></.topbar>
</header>
@ -25,7 +25,7 @@
<hr class="w-full hr" />
<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>
</div>
</div>

View File

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

View File

@ -6,9 +6,9 @@ defmodule MemexWeb.ErrorView do
def template_not_found(error_path, _assigns) do
error_string =
case error_path do
"404.html" -> dgettext("errors", "not found")
"401.html" -> dgettext("errors", "unauthorized")
_ -> dgettext("errors", "internal server error")
"404.html" -> dgettext("errors", "Not found")
"401.html" -> dgettext("errors", "Unauthorized")
_ -> dgettext("errors", "Internal Server Error")
end
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],
preferred_cli_env: [test: :test, "test.all": :test],
# ExDoc
name: "memEx",
source_url: "https://gitea.bubbletea.dev/shibao/memEx",
homepage_url: "https://gitea.bubbletea.dev/shibao/memEx",
name: "memex",
source_url: "https://gitea.bubbletea.dev/shibao/memex",
homepage_url: "https://gitea.bubbletea.dev/shibao/memex",
docs: [
# The main page in the docs
main: "README.md",

View File

@ -10,148 +10,95 @@
msgid ""
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:30
#, 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"
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_reset_password/new.html.heex:3
#: lib/memex_web/templates/user_session/new.html.heex:44
#, elixir-autogen, elixir-format
msgid "Forgot your password?"
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
msgid "Resend confirmation instructions"
#: lib/memex_web/live/invite_live/index.html.heex:11
msgid "Invite someone new!"
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
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/components/topbar.ex:119
#: lib/memex_web/templates/user_confirmation/new.html.heex:29
#: lib/memex_web/templates/user_registration/new.html.heex:48
#: lib/memex_web/templates/user_registration/new.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_session/new.html.heex:3
#: lib/memex_web/templates/user_session/new.html.heex:32
#, elixir-autogen, elixir-format
msgid "log in"
msgid "Log in"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new context"
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/components/topbar.ex:111
#: lib/memex_web/templates/user_confirmation/new.html.heex:24
#: 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_reset_password/edit.html.heex:43
#: lib/memex_web/templates/user_reset_password/new.html.heex:25
#: lib/memex_web/templates/user_session/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "register"
#: lib/memex_web/templates/user_reset_password/edit.html.heex:42
#: lib/memex_web/templates/user_reset_password/new.html.heex:24
#: lib/memex_web/templates/user_session/new.html.heex:39
msgid "Register"
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
msgid "save"
#: lib/memex_web/templates/user_confirmation/new.html.heex:3
#: lib/memex_web/templates/user_confirmation/new.html.heex:15
msgid "Resend confirmation instructions"
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
msgid "back"
#: lib/memex_web/templates/user_reset_password/edit.html.heex:3
#: lib/memex_web/templates/user_reset_password/edit.html.heex:33
msgid "Reset password"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:129
#, elixir-autogen, elixir-format
msgid "add step"
#: lib/memex_web/live/invite_live/form_component.html.heex:28
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 ""

View File

@ -10,627 +10,297 @@
msgid ""
msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:90
#, 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"
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:8
#, 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
msgid "Confirm your account"
msgstr ""
#: lib/memex_web/templates/user_registration/new.html.heex:36
#, 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_settings/edit.html.heex:126
msgid "English"
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:9
#, 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
msgid "Forgot your password?"
msgstr ""
#: lib/memex_web/templates/user_session/new.html.heex:27
#, 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
msgid "Keep me logged in for 60 days"
msgstr ""
#: lib/memex_web/templates/user_registration/new.html.heex:32
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:32
msgid "Language"
msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:37
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/live.html.heex:37
msgid "Loading..."
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:20
#, 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
msgid "Name"
msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:50
#, 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
msgid "Reconnecting..."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:36
#, 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
msgid "Reset your password"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:10
#, 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/templates/user_settings/edit.html.heex:3
msgid "Settings"
msgstr ""
#: lib/memex_web/components/user_card.ex:33
#, elixir-autogen, elixir-format
#: lib/memex_web/components/user_card.ex:30
msgid "User registered on"
msgstr ""
#: lib/memex_web/components/invite_card.ex:19
#, 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:"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:24
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:24
msgid "Uses left"
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
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/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"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:51
#, elixir-autogen, elixir-format
msgid "org-mode"
#: lib/memex_web/live/home_live.html.heex:87
msgid "Admins:"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:20
#, elixir-autogen, elixir-format
msgid "some things that this memex is very loosely inspired by:"
#: lib/memex_web/live/note_live/form_component.html.heex:20
msgid "Content"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:88
#, elixir-autogen, elixir-format
msgid "spoons? probably not. a particular brand of spoons that you really like? why not :)"
#: lib/memex_web/live/home_live.html.heex:134
msgid "Get involved!"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:14
#, elixir-autogen, elixir-format
msgid "this is a memex, used to document not just your notes, but also your perspectives and processes."
#: lib/memex_web/live/home_live.html.heex:151
msgid "Help translate"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:96
#, elixir-autogen, elixir-format
msgid "what should my contexts be like?"
#: lib/memex_web/live/home_live.html.heex:82
msgid "Instance Information"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:80
#, elixir-autogen, elixir-format
msgid "what should my notes be like?"
#: lib/memex_web/live/home_live.html.heex:113
msgid "Invite Only"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:110
#, elixir-autogen, elixir-format
msgid "what should my pipelines be like?"
#: lib/memex_web/live/home_live.html.heex:112
msgid "Public Signups"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:59
#, elixir-autogen, elixir-format
msgid "why split up into notes, contexts and pipelines?"
#: lib/memex_web/live/home_live.html.heex:160
msgid "Report bugs or request features"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:41
#, elixir-autogen, elixir-format
msgid "zettelkasten"
#: lib/memex_web/live/note_live/form_component.html.heex:35
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 ""

View File

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

View File

@ -10,123 +10,109 @@
msgid ""
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:84
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:84
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/memex_web/templates/error/error.html.heex:8
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/error/error.html.heex:8
msgid "Error"
msgstr ""
#: lib/memex_web/controllers/user_session_controller.ex:17
#, 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
msgid "Invalid email or password"
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_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
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:119
msgid "Oops, something went wrong! Please check the errors below."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:63
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:63
msgid "Reset password link is invalid or it has expired."
msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:24
#: lib/memex_web/controllers/user_registration_controller.ex:55
#, 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"
msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:14
#: lib/memex_web/controllers/user_registration_controller.ex:45
#, 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"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:99
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:99
msgid "Unable to delete user"
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:54
#, 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
msgid "User confirmation link is invalid or it has expired."
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:18
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:18
msgid "You are not authorized to view this page"
msgstr ""
#: lib/memex_web/controllers/user_auth.ex:177
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_auth.ex:177
msgid "You are not authorized to view this page."
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_auth.ex:39
#: 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."
msgstr ""
#: lib/memex/accounts/user.ex:129
#, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:130
msgid "did not change"
msgstr ""
#: lib/memex/accounts/user.ex:150
#, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:151
msgid "does not match password"
msgstr ""
#: lib/memex/accounts/user.ex:187
#, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:188
msgid "is not valid"
msgstr ""
#: lib/memex/accounts/user.ex:85
#, elixir-autogen, elixir-format
#: lib/memex/accounts/user.ex:84
msgid "must have the @ sign and no spaces"
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,149 +10,138 @@
msgid ""
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:38
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:38
msgid "%{email} confirmed successfully."
msgstr ""
#: lib/memex_web/live/invite_live/form_component.ex:62
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.ex:62
msgid "%{invite_name} created successfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:53
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:53
msgid "%{invite_name} deleted succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:114
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:114
msgid "%{invite_name} disabled succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:90
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:90
msgid "%{invite_name} enabled succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:68
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:68
msgid "%{invite_name} updated succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.ex:42
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.ex:42
msgid "%{invite_name} updated successfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:139
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:139
msgid "%{user_email} deleted succesfully"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:29
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:29
msgid "A link to confirm your email change has been sent to the new address."
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:127
#, 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
msgid "Copied to clipboard"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:77
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:77
msgid "Email changed successfully."
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:23
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:23
msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:24
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:24
msgid "If your email is in our system, you will receive instructions to reset your password shortly."
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:65
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:65
msgid "Language updated successfully."
msgstr ""
#: lib/memex_web/controllers/user_session_controller.ex:23
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_session_controller.ex:23
msgid "Logged out successfully."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:46
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:46
msgid "Password reset successfully."
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:49
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:49
msgid "Password updated successfully."
msgstr ""
#: lib/memex_web/controllers/user_registration_controller.ex:73
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:74
msgid "Please check your email to verify your account"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:30
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:30
msgid "Saving..."
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:95
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:95
msgid "Your account has been deleted"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:133
#, elixir-autogen, elixir-format
msgid "are you sure you want to change your language?"
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}"
#: lib/memex_web/live/home_live.html.heex:91
msgid "Register to setup %{name}"
msgstr ""

View File

@ -6,30 +6,10 @@ defmodule Memex.Repo.Migrations.CreateContexts do
add :id, :binary_id, primary_key: true
add :title, :string
add :content, :text
add :tags, {:array, :string}
add :tag, {:array, :string}
add :visibility, :string
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
timestamps()
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

View File

@ -6,30 +6,9 @@ defmodule Memex.Repo.Migrations.CreatePipelines do
add :id, :binary_id, primary_key: true
add :title, :string
add :description, :text
add :tags, {:array, :citext}
add :visibility, :string
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
timestamps()
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

View File

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

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,16 @@
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

@ -1,14 +0,0 @@
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,8 +1,6 @@
# memEx
# Memex
![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.
memex is an easy way to digitize the structured processes of your life.
- Notes: Document notes about individual items or concepts
- Contexts: Provide context around a single topic and hotlink to individual
@ -18,7 +16,7 @@ memEx is an easy way to digitize the structured processes of your life.
# Installation
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.
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
@ -29,8 +27,8 @@ The first created user will be created as an admin.
# Configuration
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).
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).
- `HOST`: External url to generate links with. Must be set with your hosted
domain name! I.e. `memex.mywebsite.tld`
@ -57,6 +55,4 @@ You can use the following environment variables to configure memEx in
---
[![Build
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/)
Status](https://drone.bubbletea.dev/api/badges/shibao/memex/status.svg?ref=refs/heads/dev)](https://drone.bubbletea.dev/shibao/memex)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,148 +1,68 @@
defmodule Memex.StepsTest do
use Memex.DataCase
import Memex.{PipelinesFixtures, StepsFixtures}
alias Memex.Pipelines.{Steps, Steps.Step}
@moduletag :steps_test
@invalid_attrs %{content: nil, title: nil}
alias Memex.Steps
describe "steps" do
setup do
user = user_fixture()
pipeline = pipeline_fixture(user)
alias Memex.Steps.Step
[user: user, pipeline: pipeline]
import Memex.StepsFixtures
@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
test "list_steps/2 returns all steps for a user", %{pipeline: pipeline, user: user} do
step_a = step_fixture(0, pipeline, user)
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]
test "get_step!/1 returns the step with given id" do
step = step_fixture()
assert Steps.get_step!(step.id) == step
end
test "get_step!/2 returns the step with given id", %{pipeline: pipeline, user: user} do
step = step_fixture(0, pipeline, user)
assert Steps.get_step!(step.id, user) == step
end
test "create_step/1 with valid data creates a step" do
valid_attrs = %{description: "some description", position: 42, title: "some title"}
test "get_step!/2 only returns unlisted or public steps for other users", %{user: user} do
another_user = user_fixture()
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 {:ok, %Step{} = step} = Steps.create_step(valid_attrs)
assert step.description == "some description"
assert step.position == 42
assert step.title == "some title"
end
test "create_step/4 with invalid data returns error changeset",
%{pipeline: pipeline, user: user} do
assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs, 0, pipeline, user)
test "create_step/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs)
end
test "update_step/3 with valid data updates the step", %{pipeline: pipeline, user: user} do
step = step_fixture(0, pipeline, user)
test "update_step/2 with valid data updates the step" do
step = step_fixture()
update_attrs = %{
"content" => "some updated content",
"title" => "some updated title"
description: "some updated description",
position: 43,
title: "some updated title"
}
assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs, user)
assert step.content == "some updated content"
assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs)
assert step.description == "some updated description"
assert step.position == 43
assert step.title == "some updated title"
end
test "update_step/3 with invalid data returns error changeset", %{
pipeline: pipeline,
user: user
} 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)
test "update_step/2 with invalid data returns error changeset" do
step = step_fixture()
assert {:error, %Ecto.Changeset{}} = Steps.update_step(step, @invalid_attrs)
assert step == Steps.get_step!(step.id)
end
test "delete_step/2 deletes the step", %{pipeline: pipeline, user: user} do
step = step_fixture(0, pipeline, user)
assert {:ok, %Step{}} = Steps.delete_step(step, user)
assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id, user) end
test "delete_step/1 deletes the step" do
step = step_fixture()
assert {:ok, %Step{}} = Steps.delete_step(step)
assert_raise Ecto.NoResultsError, fn -> Steps.get_step!(step.id) end
end
test "delete_step/2 moves past steps up", %{pipeline: pipeline, user: user} do
first_step = step_fixture(0, pipeline, user)
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)
test "change_step/1 returns a step changeset" do
step = step_fixture()
assert %Ecto.Changeset{} = Steps.change_step(step)
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,50 +1,29 @@
defmodule MemexWeb.PipelineLiveTest do
use MemexWeb.ConnCase
import Phoenix.LiveViewTest
import Memex.{PipelinesFixtures, StepsFixtures}
import Memex.PipelinesFixtures
@create_attrs %{
"description" => "some description",
"tags_string" => "tag1",
"slug" => "some-slug",
"visibility" => :public
}
@create_attrs %{description: "some description", title: "some title", 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
description: "some updated description",
title: "some updated title",
visibility: :private
}
@invalid_attrs %{description: nil, title: nil, visibility: nil}
defp create_pipeline(%{user: user}) do
[pipeline: pipeline_fixture(user)]
defp create_pipeline(_) do
pipeline = pipeline_fixture()
%{pipeline: pipeline}
end
describe "Index" do
setup [:register_and_log_in_user, :create_pipeline]
setup [:create_pipeline]
test "lists all pipelines", %{conn: conn, pipeline: pipeline} do
{:ok, _index_live, html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "pipelines"
assert html =~ "listing pipelines"
assert html =~ pipeline.description
end
@ -66,17 +45,17 @@ defmodule MemexWeb.PipelineLiveTest do
|> render_submit()
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "#{@create_attrs |> Map.get("slug")} created"
assert html =~ "pipeline created successfully"
assert html =~ "some description"
end
test "updates pipeline in listing", %{conn: conn, pipeline: pipeline} do
{:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert index_live |> element("[data-qa=\"pipeline-edit-#{pipeline.id}\"]") |> render_click() =~
"edit"
assert index_live |> element("#pipeline-#{pipeline.id} a", "edit") |> render_click() =~
"edit pipeline"
assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline.slug))
assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline))
assert index_live
|> form("#pipeline-form", pipeline: @invalid_attrs)
@ -88,37 +67,35 @@ defmodule MemexWeb.PipelineLiveTest do
|> render_submit()
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
assert html =~ "pipeline updated successfully"
assert html =~ "some updated description"
end
test "deletes pipeline in listing", %{conn: conn, pipeline: pipeline} do
{:ok, index_live, _html} = live(conn, Routes.pipeline_index_path(conn, :index))
assert index_live
|> element("[data-qa=\"delete-pipeline-#{pipeline.id}\"]")
|> render_click()
assert index_live |> element("#pipeline-#{pipeline.id} a", "delete") |> render_click()
refute has_element?(index_live, "#pipeline-#{pipeline.id}")
end
end
describe "show" do
setup [:register_and_log_in_user, :create_pipeline]
setup [:create_pipeline]
test "displays pipeline", %{conn: conn, pipeline: pipeline} do
{:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
{:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline))
assert html =~ "pipeline"
assert html =~ "show pipeline"
assert html =~ pipeline.description
end
test "updates pipeline within modal", %{conn: conn, pipeline: pipeline} do
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline))
assert show_live |> element("a", "edit") |> render_click() =~ "edit"
assert show_live |> element("a", "edit") |> render_click() =~
"edit pipeline"
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline.slug))
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline))
assert show_live
|> form("#pipeline-form", pipeline: @invalid_attrs)
@ -126,129 +103,12 @@ defmodule MemexWeb.PipelineLiveTest do
{:ok, _, html} =
show_live
|> form("#pipeline-form", pipeline: Map.put(@update_attrs, "slug", pipeline.slug))
|> form("#pipeline-form", pipeline: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline))
assert html =~ "#{pipeline.slug} saved"
assert html =~ "pipeline updated successfully"
assert html =~ "some updated description"
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

View File

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

View File

@ -2,7 +2,8 @@ defmodule Memex.Fixtures do
@moduledoc """
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 valid_user_password, do: "hello world!"
@ -25,12 +26,11 @@ defmodule Memex.Fixtures do
attrs
|> Enum.into(%{
"email" => unique_user_email(),
"password" => valid_user_password()
"password" => valid_user_password(),
"role" => "admin"
})
|> Accounts.register_user()
|> unwrap_ok_tuple()
|> User.role_changeset("admin")
|> Repo.update!()
end
def extract_user_token(fun) do
@ -56,14 +56,5 @@ defmodule Memex.Fixtures do
})
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
end

View File

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

View File

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

View File

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

View File

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